Asynchronous Javascript 3 - Promise


Masih ingat dengan neraka callback dari artikel sebelumnya? Perkenankan Saya untuk mengingatkanmu pada kengerian itu.

getData(baseUrl + "/comments/1", (err, data) => {
  if (err) {
    console.log(err);
  } else {
    const postId = data.postId;
    getData(baseUrl + "/posts/" + postId, (err, data) => {
      if (err) {
        console.log(err);
      } else {
        const userId = data.userId;
        getData(baseUrl + "/users/" + userId, (err, data) => {
          if (err) {
            console.log(err);
          } else {
            document.getElementById("display").innerHTML = JSON.stringify(
              data,
              null,
              4,
            );
          }
        });
      }
    });
  }
});

Tapi jangan takut, karena di artikel ini Saya akan membahas tentang cara untuk keluar dari neraka callback dengan menggunakan Promise.

Apa itu Promise?

Bayangkan kamu sedang memesan makanan dari sebuah restoran favoritmu melalui aplikasi online. Ketika memesan, kamu tidak langsung mendapatkan makananmu; ada beberapa langkah yang harus dilalui seperti menyiapkan, memasak, dan mengantarkan makanan tersebut ke rumahmu. Selama proses ini, kamu bisa melakukan hal lain seperti menonton TV, membaca buku, atau bekerja, tanpa harus menunggu di depan pintu.

Dalam skenario ini, aplikasi pemesanan makanan memberi kamu sebuah “janji” bahwa makananmu akan tiba. Kamu mungkin juga mendapatkan pembaruan status seperti “makanan sedang disiapkan”, “makanan sedang diantar”, dan akhirnya “makanan telah tiba”.

Begitu pula dalam JavaScript, Promise adalah cara untuk mengelola operasi asynchronous. Ketika kamu membuat sebuah Promise, itu seperti membuat janji bahwa suatu nilai atau hasil akan tersedia di masa depan. Pada prakteknya, Promise bisa berada dalam salah satu dari tiga keadaan (state):

  • Pending (Menunggu): Keadaan ketika Promise masih menunggu hasil dari suatu proses asynchronous, belum terpenuhi ataupun tertolak..
  • Fulfilled (Terpenuhi): Ketika Promise telah mendapatkan hasil yang diharapkan.
  • Rejected (Ditolak): Ketika terjadi kesalahan dan Promise tidak dapat memberikan hasil yang diharapkan.

Dengan menggunakan Promise, kamu bisa menulis kode yang lebih bersih dan mudah dipahami dibandingkan dengan callback tradisional. Kamu bisa menggunakan metode .then() untuk menangani hasil yang sukses dan .catch() untuk menangani error.

const janjiMakanan = new Promise((resolve, reject) => {
  resolve("Selamat makan");
  // reject('Toko kehabisan makanan')
});

janjiMakanan
  .then((makanan) => {
    console.log(`Makanan telah tiba: ${makanan}`);
  })
  .catch((error) => {
    console.log(`Terjadi kesalahan: ${error}`);
  });

Promise tidak tersedia sedari awal Javascript diciptakan. Itulah mengapa ada masalah seperti callback hell yang muncul, karena hanya pola callback yang dahulu tersedia. Pada tahun 2012 proposal spesifikasi untuk Promise dibuat, dan baru pada tahun 2015 distandardisasi secara formal pada ECMAScript2015, lalu diimplementasi.

Bagaimana cara menangani Promise?

Pada dasarnya Promise adalah sebuah objek yang menyimpan nilai yang dijanjikan akan ada pada waktu tertentu. Promise memiliki 3 buah instance method yaitu:

  • .then(), sebuah metode yang akan berjalan jika status Promise telah terpenuhi, dan akan mengeksekusi fungsi callback didalamnya dengan argumen berisi value dari Promise. Metode .then() dapat menerima 2 argumen, yaitu callback terpenuhi dan ditolak.
  • .catch(): Metode yang akan berjalan jika status Promise telah ditolak, dan akan mengeksekusi fungsi callback di dalamnya dengan argumen berisi alasan penolakan.
  • .finally(): Metode yang akan berjalan ketika semua callback .then() dan .catch() telah selesai dieksekusi. Callback pada metode ini tidak menerima argumen apapun dan digunakan untuk melakukan tugas pembersihan atau tindakan akhir lainnya.

Kode berikut adalah contoh penanganan Promise menggunakan API fetch() yang mengembalikan sebuah Promise sehingga tidak mem-block eksekusi fungsi lainnya.

const baseUrl = "https://jsonplaceholder.typicode.com";

console.log("code starting...");
// mendapatkan data komentar
fetch(baseUrl + "/comments/1")
  .then((response) => response.json())
  // mendapatkan data post
  .then((data) => fetch(baseUrl + "/posts/" + data.postId))
  .then((response) => response.json())
  // mendapatkan data user
  .then((data) => fetch(baseUrl + "/users/" + data.userId))
  .then((response) => response.json())
  .then((data) => {
    console.log("user fetched...");
    console.log(data);
  })
  // menghandle error yang muncul dari semua Promise sebelumnya
  .catch((error) => {
    console.log(error);
  })
  .finally(() => {
    console.log("promise end...");
  });
console.log("code finishing...");

Pola saling sambung menyambung antara metode dari Promise disebut Chaining. Hal ini mungkin karena setiap metode .then() dan .catch() mengembalikan sebuah Promise juga. Diagram berikut menggambarkan alur berjalannya Promise.

Kalau kamu sudah pernah belajar tentang pemrograman berorientasi objek dalam Javascript, kamu bisa membuat pola chaining dengan mudah, lihat kode berikut:

class Pertambahan {
  constructor() {
    this.jumlah = 0;
  }

  tambah(angka) {
    this.jumlah += angka;
    // mengembalikan objek pertambahan
    return this;
  }

  hasil() {
    return this.jumlah;
  }
}

const hitung = new Pertambahan();
const hasil = hitung.tambah(3).tambah(6).tambah(1).hasil();
console.log(hasil);

Eksekusi callback Promise didalam runtime

Pada artikel pertama tentang pengenalan pemrograman asynchronous, telah dibahas tentang javascript runtime, atau lingkungan tempat Javascript dijalankan.

Promise dan setTimeout() sama-sama memiliki callback yang akan dipanggil secara asynchronous di masa depan. Apakah keduanya berbagi tempat antrian? Jika ya, tentu callbacknya akan dipanggil berurutan. Mari buktikan, coba terka output dari kode berikut

console.log("1");
setTimeout(() => {
  console.log("2");
});
setTimeout(() => {
  console.log("3");
}, 100);
Promise.resolve().then(() => {
  console.log("4");
});
console.log("5");

Setelah dijalankan, ternyata output dari Promise muncul terlebih dahulu daripada setTimeout(). Hal ini karena Promise memiliki queuenya sendiri yang bernama MicroTask queue. Berikut adalah simulasi kode sebelumnya pada Javascript runtime.

Javascript Engine

Heap

Call Stack

Event Loop

Web API

API

Task Queue

MicroTask Queue

Memahami urutan task asynchronous pada bahasa Javascript memang cukup menantang, mirip seperti masalah specificity pada CSS. Namun ini sangatlah penting, karena banyak masalah pada pengembangan aplikasi Javascript bisa diselesaikan dengan pemrograman asynchronous.

Membuat Promise-mu sendiri

Membuat sebuah Promise sangatlah mudah (yang sulit memahaminnya 😀). Sebelumnya Saya telah membuat sebuah Promise bernama janjiMakanan yang memiliki nilai terpenuhi 'Selamat makan'.

let janjiMakanan = new Promise((resolve, reject) => {
  resolve("Selamat makan");
});

Promise janjiMakanan adalah instance yang dibuat dari promise constructor. Ia menerima callback yang akan dipanggil dengan 2 buah argumen berupa fungsi resolve() dan reject(). Penamaan kedua fungsi tersebut bersifat konvensional, kamu bebas menamainya apa saja. Berikut adalah kegunaanya:

  • resolve(nilai) Fungsi ini digunakan untuk mengubah status Promise menjadi fulfilled (terpenuhi) dan mengembalikan nilai yang dijanjikan yang akan ditangani oleh metode .then().
  • reject(alasan): Fungsi ini digunakan untuk mengubah status Promise menjadi rejected (ditolak) dan mengembalikan alasan penolakan. Ketika reject dipanggil, Promise akan gagal dan alasan yang diberikan sebagai argumen ke callback dalam metode .catch().

Ketika membuat instance promise baru, callback yang diberikan sebagai argumen dijalankan secara synchronous. Sehingga jika terjadi proses yang berat akan tetap memblocking eksekusi kode selanjutnya.

Metode statik Promise

konstruktor Promise memiliki beberapa metode statik yang bisa digunakan tanpa perlu membuat sebuah objek Promise (dengan new Promise()). Berikut adalah metode-metode tersebut.

  • Promise.resolve() Mengembalikan sebuah Promise yang langsung terpenuhi dengan nilai yang diberikan.

  • Promise.reject() Mengembalikan sebuah Promise yang langsung ditolak dengan alasan yang diberikan.

  • Promise.all() Menerima argumen berupa array berisi Promise dan mengembalikan sebuah Promise yang akan terpenuhi jika semua Promise terpenuhi, dan ditolak apabila ada satu Promise yang ditolak.

    const promises = [Promise.resolve(1), Promise.resolve(2)];
    // const promises = [Promise.reject(1), Promise.resolve(2)];
    
    Promise.all(promises)
      .then((values) => console.log('value:', values))
      .catch((error) => console.log('error:', error));
  • Promise.allSettled() Juga menerima argumen berupa array berisi Promise dan mengembalikan sebuah Promise yang akan terpenuhi jika semua Promise telah selesai, tak peduli jika hasilnya terpenuhi atau ditolak.

    const promises = [Promise.resolve(1), Promise.reject(2)];
    // const promises = [Promise.reject(1), Promise.reject(2)];
    
    Promise.allSettled(promises)
      .then((values) => console.log("value:", values))
      .catch((error) => console.log("error:", error));
  • Promise.race() Menerima argumen berupa array dan mengembalikan sebuah Promise yang akan terpenuhi ketika satu Promise selesai, baik itu terpenuhi ataupun ditolak.

    const promises = [Promise.resolve(1), Promise.reject(2)];
    // const promises = [Promise.reject(1), Promise.resolve(2)];
    
    Promise.race(promises)
      .then((values) => console.log("value:", values))
      .catch((error) => console.log("error:", error));
  • Promise.any() Menerima argumen berupa array dan mengembalikan sebuah Promise yang akan terpenuhi ketika satu Promise terpenuhi. Hanya ditolak ketika semua Promise ditolak dan memberikan AggregateError

    const promises = [Promise.reject(1), Promise.resolve(2)];
    // const promises = [Promise.rreject(1), Promise.reject(2)];
    
    Promise.any(promises)
      .then((values) => console.log("value:", values))
      .catch((error) => console.log("error:", error));

Aturan-aturan utama Promise

Berikut adalah beberapa aturan utama Promise yang perlu diperhatikan agar dapat menggunakan Promise dengan baik.

  1. Jika callback mengembalikan Promise lain, maka eksekusi .then() selanjutnya akan menunggu sampai Promise tersebut terpenuhi (atau ditolak).
function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

Promise.resolve("sukses")
  .then((e) => {
    console.log(e);
    return sleep(2000);
  })
  .then((e) => console.log("2 detik kemudian"))
  .catch((er) => console.log(er));
  1. Jika callback pada metode .then() sebelumnya berupa promise, pastikan untuk melakukan pengembalian, jika tidak, metode .then() selanjutnya tidak dapat melacak keterpenuhan Promise, dan Promise menjadi floating.
function sleepWithValue(ms, value) {
  return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}

Promise.resolve().then(() => {
  sleepWithValue(2000, 'sukses!')
}).then((hasil) => {
  console.log(hasil)
})
  1. Promise lebih baik ditangani secara flat dibanding nested, karena nanti akan lebih sulit dibaca.
// hindari *nesting*
fetch("url1").then((response1) => {
  return fetch("url2").then((response2) => {
    //...
  });
});

// Pilih bentuk *flat*
fetch("url1")
  .then((response1) => fetch("url2"))
  .then((response2) => {
    //...
  });
  1. Callback yang dijadikan argumen untuk metode .then() tidak akan pernah dipanggil secara synchronous meski Promise-nya sudah terpenuhi.
console.log("start");
Promise.resolve("sukses").then((hasil) => console.log(hasil));
console.log("end"); // start, end, sukses
  1. Hanya nilai terpenuhi atau tertolak pertama yang akan dikembalikan oleh suatu Promise.
new Promise((resolve, reject) => {
  resolve("1");
  resolve("2");
}).then((hasil) => {
  console.log(hasil); // '1'
});

Istilah dalam Promise

Banyak istilah (term) terkait Promise yang mirip satu sama lain sehingga membuat bingugn. Terlebih ketika diterjemahkan ke bahasa Indonesia , terkadang ada konteks yang hilang.

Berikut adalah daftar istilah terkait Promise dalam bahasa Inggris, translasi, dan penjelasannya.

engidketerangan
pendingmenungguKeadaan ketika Promise masih menunggu hasil dari suatu proses asynchronous, belum terpenuhi ataupun tertolak.
fulfilledterpenuhiKetika Promise sudah mendapatkan hasil dan metode .then() menjalankan callback didalamnya.
rejectedditolak/tertolakKeadaan saat ada suatu hal yang membuat Promise tidak mendapatkan hasil yang diinginkan (error) dan metode .catch menjalankan callback didalamnya.
settledselesaibukan merupakan suatu keadaan (state) tertentu, hanya istilah kebahasaan untuk kondisi Promise sudah tidak menunggu, entah terpenuhi ataupun tertolak.
resolvedtuntasBiasanya, Promise yang tuntas ialah Promise yang sudah selesai, alias sudah terpenuhi ataupun tertolak. Namun terkadang sebuah Promise dituntaskan dengan Promise yang lain, maka keadaanya akan mengikuti Promise yang dijadikan argumen penuntasan.

Jika masih belum klik, kamu bisa juga melihat dengan sudut pandang state and fate (keadaan dan takdir). Menunggu, terpenuhi dan tertolak adalah keadaan dari suatu Promise. Sedangkan tuntas ataupun tidak tuntas (unresolved) adalah takdir dari Promise.

Promise yang tuntas ialah Promise yang apabila di resolve() ataupun reject() sudah tidak berdampak apa-apa, karena Promise tersebut sudah terpenuhi atau tertolak sebelumnya (ingat, Promise hanya bisa di resolve() satu kali).

Dibawah adalah ilustrasi untuk menggambarkan keadaan sebuah Promise.

Promise map

resolve() memiliki garis ke lingkaran putih dan tidak langsung terpenuhi karena jika resolve() mendapatkan argumen berupa Promise, hasil akhir belum diketahui.

referensi:


Itulah sedikit tentang Promise. Masih banyak detail yang bisa dipelajari tentang Promise dan penanganannya di internet. Setelah ini Saya akan membahas satu hal lagi terkait Promise yaitu async/await.

Referensi