Chapter sebelumnya menutup dengan satu keterbatasan penting: try-catch hanya menangkap error yang terjadi pada saat pemanggilan berlangsung, sedangkan operasi andalan Node.js — membaca berkas, memanggil API, menjalankan query — mengembalikan hasilnya belakangan, setelah function pemanggil sudah pulang. Untuk dunia yang tertunda inilah JavaScript menyediakan promise: object perwakilan hasil yang belum tentu sudah ada, yang nanti terpenuhi dengan nilai atau ditolak dengan error. Di atasnya, async-await memungkinkan kita menulis kode asynchronous yang tampak seperti kode biasa. Pada chapter ini kita akan membedah keduanya sampai pola error propagation-nya, dengan contoh yang seluruhnya bisa dijalankan langsung.

Promise dan Tiga Keadaannya #

Promise adalah object dengan siklus hidup yang ketat. Ia lahir dalam keadaan pending, lalu menetap (settle) tepat satu kali: menjadi fulfilled membawa nilai hasil, atau rejected membawa error — dan begitu menetap, keadaannya tidak pernah berubah lagi. Callback yang kita daftarkan setelah promise menetap tetap dijalankan; mendaftar lewat .then() lebih awal atau lebih akhir tidak mengubah hasilnya. Karena promise hanyalah object, ia bisa disimpan ke variabel, dikirim sebagai argumen, dan dikembalikan dari function — inilah fondasi seluruh API asynchronous Node.js modern, dari fs/promises sampai fetch.

Membuat dan Mengonsumsi Promise #

Constructor new Promise(executor) menerima satu function yang dijalankan seketika dengan dua parameter: resolve untuk menyerahkan nilai hasil, dan reject untuk melempar kegagalan. Sisi konsumen memakai tiga method: .then() menerima nilai, .catch() menangani penolakan, dan .finally() berjalan di kedua jalur. Berkas promise-dasar.mjs memperlihatkan satu function pembuat promise yang dipakai dalam dua skenario:

js
function ambilPengguna(id) {
  return new Promise((resolve, reject) => {
    if (id <= 0) {
      reject(new Error(`id tidak valid: ${id}`));
      return;
    }
    resolve({ id, nama: `Pengguna ${id}` });
  });
}

const sukses = ambilPengguna(7);
const gagal = ambilPengguna(-1);

console.log("dua promise sudah dibuat");

sukses
  .then((pengguna) => console.log(`then    : ${pengguna.nama}`))
  .catch((e) => console.log(`catch   : ${e.message}`))
  .finally(() => console.log("finally : alur sukses selesai"));

gagal
  .then((pengguna) => console.log(`then    : ${pengguna.nama}`))
  .catch((e) => console.log(`catch   : ${e.name}: ${e.message}`))
  .finally(() => console.log("finally : alur gagal selesai"));

Output yang Dihasilkan:

text
dua promise sudah dibuat
then    : Pengguna 7
catch   : Error: id tidak valid: -1
finally : alur sukses selesai
finally : alur gagal selesai

Mari kita baca output dan kodenya bersama:

  1. "dua promise sudah dibuat" tercetak lebih dulu: function executor berjalan sinkron saat new Promise(...) dipanggil, tetapi callback .then() dan .catch() dijadwalkan sebagai microtask — mereka baru berjalan setelah kode sinkron selesai. Urutan ini stabil dan bisa diandalkan.
  2. Promise sukses berakhir di then: nilai { id: 7, nama: "Pengguna 7" } mengalir ke callback; .catch() dilewati; .finally() tetap berjalan.
  3. Promise gagal melompati then: penolakan menjalar sampai menemui .catch(), yang menerima object Error — persis seperti throw sinkron, sehingga pola e.name dan e.message dari chapter sebelumnya tetap berlaku.

Method .then() mengembalikan promise baru yang menampung nilai kembalian callback-nya, sehingga pemrosesan bertahap bisa dirangkai menjadi rantai:

js
Promise.resolve(7)
  .then((n) => n * 6)
  .then((n) => `hasil: ${n}`)
  .then(console.log);

Output yang Dihasilkan:

text
hasil: 42

Nilai 7 dikalikan menjadi 42 pada tautan pertama, dirangkai menjadi string pada tautan kedua, lalu dicetak pada tautan ketiga. Setiap .then() menunggu tautan sebelumnya selesai — perhatikan pula Promise.resolve(), helper untuk membungkus nilai biasa menjadi promise yang sudah terpenuhi, yang kita pakai untuk menciptakan contoh tanpa operasi eksternal.

async dan await #

Kata kunci async mengubah sebuah function menjadi async function dengan dua jaminan: nilai yang dikembalikan otomatis dibungkus menjadi promise, dan error yang terlempar di dalamnya menjadi penolakan promise alih-alih exception langsung. Bukti singkatnya:

js
async function hitung() {
  return 40 + 2;
}

hitung().then((hasil) => console.log(`hasil: ${hasil}`));

Output yang Dihasilkan:

text
hasil: 42

Function hitung mengembalikan 42 seperti function biasa, tetapi pemanggilnya menerima promise — itulah mengapa kita bisa memasang .then() pada hasilnya. Di dalam async function, kata kunci await menjeda eksekusi sampai promise yang ditunggu menetap: kalau terpenuhi, await menghasilkan nilainya; kalau ditolak, await melempar error yang bisa ditangkap try-catch biasa. Berkas async-await.mjs merangkum semuanya:

js
function ambilPengguna(id) {
  return new Promise((resolve, reject) => {
    if (id <= 0) {
      reject(new Error(`id tidak valid: ${id}`));
      return;
    }
    resolve({ id, nama: `Pengguna ${id}` });
  });
}

async function tampilkanPengguna(id) {
  console.log("menyiapkan tampilan...");
  try {
    const pengguna = await ambilPengguna(id);
    console.log(`nama    : ${pengguna.nama}`);
    return "selesai";
  } catch (e) {
    console.log(`gagal   : ${e.message}`);
    return "gagal ditangani";
  }
}

const hasilA = tampilkanPengguna(7);
const hasilB = tampilkanPengguna(0);

console.log("pemanggilan selesai, keduanya masih berjalan");

Promise.all([hasilA, hasilB]).then((hasil) => {
  console.log(`hasil   : ${hasil.join(" | ")}`);
});

Output yang Dihasilkan:

text
menyiapkan tampilan...
menyiapkan tampilan...
pemanggilan selesai, keduanya masih berjalan
nama    : Pengguna 7
gagal   : id tidak valid: 0
hasil   : selesai | gagal ditangani

Urutan pada output memperlihatkan cara kerja await secara jujur:

  1. Dua baris “menyiapkan tampilan…” muncul lebih dulu: setiap pemanggilan tampilkanPengguna berjalan sinkron sampai bertemu await, lalu menjembatani kendali kembali ke pemanggil — eksekusi function ditangguhkan, tidak memblokir proses.
  2. “pemanggilan selesai…” tercetak ketika kedua await masih menggantung: baris setelah pemanggilan tidak menunggu; hasilnya berupa promise hasilA dan hasilB yang masih pending.
  3. Baris nama dan gagal menyusul: begitu promise yang ditunggu menetap, eksekusi async function berlanjut. Untuk id 0, penolakan berubah menjadi exception di titik await dan ditangkap blok catch — error asynchronous kini ditangani dengan struktur try-catch yang sama seperti Chapter Penanganan Error dengan try-catch.

Baris Promise.all([hasilA, hasilB]) menutup contoh dengan pola menjalankan banyak operasi sekaligus: Promise.all menerima array promise dan mengembalikan satu promise yang terpenuhi dengan array hasil dalam urutan yang sama — "selesai | gagal ditangani". Jika salah satu promise ditolak, Promise.all langsung menolak dengan error pertama itu; untuk kebutuhan mengumpulkan semua hasil termasuk yang gagal, ada Promise.allSettled yang akan kita pakai pada Section Asynchronous Runtime.

Error Propagation pada async-await #

Aturan penularan error pada async function sederhana namun penting. return nilai biasa → promise terpenuhi; throw di dalam body (termasuk dari await yang ditolak) → promise ditolak. Artinya, error yang tidak ditangkap try-catch di dalam async function tidak menghentikan proses seketika — penolakan mengalir ke pemanggil, yang bisa memasang .catch() atau await di dalam try sendiri. Pemanggil yang juga tidak menanganinya membuat promise tertolak tanpa penanganan (unhandled rejection), dan Node.js meresponsnya dengan laporan di stderr serta exit code yang tidak nol. Karena itu, tepi aplikasi — handler HTTP, event error pada stream, baris utama CLI — adalah tempat penanganan terakhir wajib berada.

Referensi lanjutan untuk topik chapter ini:

Promise mengantarkan satu nilai di masa depan. Bagaimana dengan urutan nilai yang tiba bertahap — baris demi baris berkas, event demi event? Untuk itu JavaScript punya protokol iterator dan generator, dua topik yang menutup rangkaian fitur bahasa pada Section ini. Lanjut ke Chapter Iterator dan Generator.