Programming

Promise dan Async/Await JavaScript: Panduan Lengkap dari Dasar

M
MUGHU
33 menit baca
Promise dan Async/Await JavaScript: Panduan Lengkap dari Dasar
Daftar isi

Asynchronous adalah salah satu hal yang bikin JavaScript terasa rumit di awal, padahal konsepnya sederhana kalau dipecah pelan-pelan. Di artikel ini kita bahas tuntas Promise dan async/await: dari kenapa mereka ada, cara pakainya, sampai jebakan yang sering bikin kode error. Semua pakai contoh yang bisa langsung kamu coba.

Ringkasan Singkat

Promise adalah objek yang mewakili hasil dari operasi asynchronous yang belum tentu selesai sekarang—bisa berhasil (fulfilled), gagal (rejected), atau masih menunggu (pending). async/await adalah sintaks yang dibangun di atas Promise supaya kode asynchronous bisa ditulis dan dibaca seperti kode biasa yang berurutan.

Intinya: async/await bukan pengganti Promise, tapi cara yang lebih enak untuk memakainya. Setiap fungsi async selalu mengembalikan Promise, dan await cuma "menunggu" Promise itu selesai.

Kenapa JavaScript Butuh Ini?

JavaScript itu single-threaded—cuma bisa ngerjain satu hal dalam satu waktu. Masalahnya, di aplikasi nyata kita sering harus ambil data dari API, baca file, atau nunggu timer. Kalau JavaScript nungguin tugas-tugas itu sambil diam, seluruh halaman bakal nge-freeze.

Solusinya: kerjakan tugas lama di "latar belakang", lanjut jalanin kode lain dulu, baru tangani hasilnya nanti pas sudah siap. Itulah inti dari asynchronous.

Coba lihat bedanya. Kode sinkron jalan urut:

JAVASCRIPT
console.log("First");
console.log("Second");
console.log("Third");
// Output:
// First
// Second
// Third

Kode asinkron bisa "loncat":

JAVASCRIPT
console.log("First");
setTimeout(() => {
  console.log("Second (setelah 2 detik)");
}, 2000);
console.log("Third");
// Output:
// First
// Third
// Second (setelah 2 detik)

setTimeout menjadwalkan kode untuk jalan nanti tanpa memblokir baris berikutnya. Nah, dari kebutuhan inilah Promise dan async/await lahir.

Prasyarat

Sebelum lanjut, pastikan kamu sudah paham:

  • Variabel dan fungsi dasar (let, const, arrow function () => {})

  • Object dan array sederhana

  • Node.js terpasang (untuk coba di luar browser) atau cukup Console di browser (tekan F12)

  • Sedikit kenalan dengan setTimeout dan fetch

Versi browser modern apa pun sudah mendukung ini. async/await resmi masuk di ECMAScript 2017 dan didukung semua browser besar sejak September 2017 (Chrome 58, Firefox 52, Safari 11, Edge 15).

Babak 1: Callback dan Asal Mula Masalah

Dulu, sebelum ada Promise, cara menangani tugas asinkron adalah callback—yaitu fungsi yang kita oper sebagai argumen, dan dipanggil nanti pas tugasnya selesai.

JAVASCRIPT
function fetchUser(id, callback) {
  setTimeout(() => {
    callback({ id: id, name: 'John Doe' });
  }, 1000);
}

fetchUser(1, (user) => {
  console.log(user); // { id: 1, name: 'John Doe' }
});

Kelihatan oke untuk satu tugas. Tapi begitu tugasnya saling bergantung, kodenya menjurang ke kanan—ini yang terkenal dengan sebutan callback hell atau pyramid of doom:

JAVASCRIPT
fetchUser(1, (user) => {
  fetchPosts(user.id, (posts) => {
    fetchComments(posts[0].id, (comments) => {
      fetchLikes(comments[0].id, (likes) => {
        console.log(likes);
        // makin dalam, makin pusing 😱
      });
    });
  });
});

Kenapa ini buruk: susah dibaca, susah di-debug, dan penanganan error-nya ribet karena tiap level butuh cek error sendiri. Dari sinilah Promise masuk sebagai penyelamat.

Babak 2: Promise

Sebuah Promise itu seperti tanda terima laundry. Kamu kasih baju kotor, dapat struk (Promise). Bajunya belum bersih sekarang, tapi struk itu janji bahwa nanti hasilnya bisa kamu ambil—entah baju bersih (fulfilled) atau kabar bahwa bajunya rusak (rejected).

Tiga Keadaan Promise

Keadaan

Arti

Pending

Keadaan awal, belum selesai

Fulfilled

Operasi berhasil, ada nilai hasil

Rejected

Operasi gagal, ada alasan error

Sekali sebuah Promise berpindah dari pending ke fulfilled atau rejected, keadaannya terkunci—nggak bisa balik lagi.

Membuat Promise

JAVASCRIPT
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const sukses = true;
    if (sukses) {
      resolve("Operasi berhasil!");
    } else {
      reject("Operasi gagal!");
    }
  }, 2000);
});

new Promise() menerima sebuah fungsi dengan dua parameter: resolve (panggil saat sukses) dan reject (panggil saat gagal). Dua nama ini sudah disediakan JavaScript, kamu tinggal pakai.

Memakai Promise: .then(), .catch(), .finally()

JAVASCRIPT
myPromise
  .then((result) => {
    console.log(result); // "Operasi berhasil!"
  })
  .catch((error) => {
    console.error(error);
  })
  .finally(() => {
    console.log("Selesai, apa pun hasilnya");
  });
  • .then() menangani kasus sukses

  • .catch() menangani error

  • .finally() jalan tetap, mau sukses atau gagal (cocok buat matiin loading spinner)

Chaining: Rantai Promise

Inilah keunggulan Promise dibanding callback. Kita bisa rangkai operasi berurutan tanpa menjurang:

JAVASCRIPT
fetchUser(1)
  .then((user) => {
    console.log('User:', user);
    return fetchPosts(user.id); // kembalikan Promise berikutnya
  })
  .then((posts) => {
    console.log('Posts:', posts);
    return fetchComments(posts[0].id);
  })
  .then((comments) => {
    console.log('Comments:', comments);
  })
  .catch((error) => {
    console.error('Error:', error); // satu catch untuk seluruh rantai
  });

Kenapa ini penting: rantai datar ke bawah jauh lebih mudah dibaca daripada piramida callback. Dan satu .catch() di ujung sudah menangkap error dari mana pun di rantai itu.

Hati-hati dengan kesalahan klasik: lupa return. Kalau kamu nggak mengembalikan Promise di dalam .then(), rantainya putus dan langkah berikutnya jalan sebelum waktunya.

JAVASCRIPT
// ❌ Salah: tidak ada return
.then((data) => {
  processData(data); // hasilnya hilang dari rantai
})
// ✅ Benar
.then((data) => {
  return processData(data);
})

Babak 3: async/await

Promise sudah bagus, tapi rantai .then() yang panjang tetap bikin pusing—apalagi kalau ada if, perulangan, atau logika rumit. async/await datang membawa rasa "sinkron" ke kode asinkron.

Aturannya cuma dua, gampang diingat:

  • async di depan fungsi bikin fungsi itu selalu mengembalikan Promise

  • await bikin eksekusi berhenti sebentar sampai Promise selesai, lalu lanjut

Fungsi async

JAVASCRIPT
async function getData() {
  return 'Hello';
}
// Sama persis dengan:
function getData() {
  return Promise.resolve('Hello');
}
getData().then((v) => console.log(v)); // 'Hello'

Kalau nilai yang kamu kembalikan bukan Promise, JavaScript otomatis membungkusnya jadi Promise. Jadi tenang, kamu nggak perlu bungkus manual.

Kata Kunci await

JAVASCRIPT
async function getUser() {
  const response = await fetch('/api/user');
  const user = await response.json();
  return user;
}

Bandingkan rantai Promise tadi dengan versi async/await—jelas lebih rapi:

JAVASCRIPT
async function fetchAllData() {
  const user = await fetchUser(1);
  console.log('User:', user);

  const posts = await fetchPosts(user.id);
  console.log('Posts:', posts);

  const comments = await fetchComments(posts[0].id);
  console.log('Comments:', comments);

  return comments;
}

Baris demi baris, dibaca dari atas ke bawah, seperti kode biasa. Inilah kenapa async/await jadi pilihan utama di kode aplikasi modern.

Penanganan Error dengan try/catch

Salah satu keuntungan terbesar async/await: kita bisa pakai try/catch yang sudah familiar—format yang sama untuk error sinkron maupun asinkron.

JAVASCRIPT
async function getUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error('Gagal ambil user:', error);
    throw error; // lempar lagi kalau perlu ditangani di atas
  } finally {
    console.log('Percobaan selesai');
  }
}

Perbandingan: Callback vs Promise vs async/await

Kriteria

Callback

Promise

async/await

Keterbacaan

Buruk, rawan callback hell

Lebih baik via chaining

Paling enak dibaca, mirip kode sinkron

Penanganan error

Manual, repot saat bersarang

.catch() terpusat

try/catch familiar

Alur kondisional (if/loop)

Susah

Lumayan ribet

Natural, pakai kontrol biasa

Debugging

Sulit

Stack trace agak membingungkan

Stack trace lebih bersih

Dukungan

Semua lingkungan

Semua lingkungan modern

ES2017+

Sifat

Fungsi argumen

Objek dengan 3 keadaan

Sintaks di atas Promise

Catatan penting: ketiganya menangani hal yang sama—hasil operasi asinkron. async/await bukan menggantikan Promise; dia syntactic sugar di atas Promise. Memahami Promise tetap wajib, karena banyak library dan API (seperti fetch dan Axios) mengembalikan Promise langsung.

Sequential vs Parallel: Bagian yang Sering Salah

Ini pelajaran mahal yang sering bikin aplikasi lambat tanpa sadar. Kalau tugas-tugasnya tidak saling bergantung, jangan await satu per satu.

JAVASCRIPT
// ❌ Lambat — total = jumlah semua waktu (misal 1.2 detik)
async function lambat() {
  const users = await fetchUsers();
  const posts = await fetchPosts();
  const comments = await fetchComments();
  return { users, posts, comments };
}

// ✅ Cepat — total = operasi terlama saja (misal 0.5 detik)
async function cepat() {
  const [users, posts, comments] = await Promise.all([
    fetchUsers(),
    fetchPosts(),
    fetchComments()
  ]);
  return { users, posts, comments };
}

Pakai await berurutan hanya kalau langkah berikutnya butuh hasil langkah sebelumnya (misal: harus ambil user dulu baru bisa ambil user.posts).

Empat Metode Statis Promise

JAVASCRIPT
await Promise.all([p1, p2, p3]);        // tunggu semua; gagal kalau ada satu yang gagal
await Promise.allSettled([p1, p2, p3]); // tunggu semua; tidak pernah gagal, kembalikan status tiap promise
await Promise.race([p1, p2, p3]);       // ambil yang pertama selesai (sukses ATAU gagal)
await Promise.any([p1, p2, p3]);        // ambil sukses pertama; gagal kalau semua gagal
  • Promise.all — cocok untuk memuat banyak data sekaligus, fail-fast

  • Promise.allSettled — cocok kalau kamu mau hasil semuanya walau sebagian gagal

  • Promise.race — cocok untuk pola timeout

  • Promise.any — cocok untuk mencoba beberapa sumber/mirror

Contoh pola timeout pakai Promise.race:

JAVASCRIPT
function timeout(ms) {
  return new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
}

async function fetchWithTimeout(url, ms = 5000) {
  return Promise.race([fetch(url), timeout(ms)]);
}

Error yang Sering Muncul + Cara Memperbaiki

1. await di luar fungsi async

JAVASCRIPT
function bad() {
  const data = await fetchData(); // ❌ SyntaxError
}

Perbaikan: tambahkan async di depan fungsi, atau pakai top-level await (hanya di dalam ES Module).

2. fetch tidak otomatis gagal saat HTTP 404/500

JAVASCRIPT
// ❌ Tidak menangani status error
const data = await fetch('/api/data').then(r => r.json());

// ✅ Cek response.ok dulu
const response = await fetch('/api/data');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();

fetch cuma menolak (reject) kalau jaringan benar-benar gagal. Status 404 atau 500 tetap dianggap "berhasil" sampai kamu cek response.ok sendiri.

3. Unhandled promise rejection

JAVASCRIPT
async function bad() {
  const data = await fetchData(); // kalau gagal, error tak tertangani
}

Perbaikan: bungkus dengan try/catch atau pasang .catch() di pemanggilnya.

4. await di dalam loop tanpa sadar bikin lambat

JAVASCRIPT
// ❌ Satu per satu
for (const item of items) {
  await processItem(item);
}
// ✅ Paralel
await Promise.all(items.map(item => processItem(item)));

Catatan: tetap pakai versi loop kalau kamu sengaja butuh urut atau ada batasan rate limit API.

5. Mencampur gaya Promise dan async/await

Pilih satu gaya dalam satu fungsi. Mencampur await dengan .then() di fungsi yang sama bikin alur dan penanganan error jadi nggak konsisten.

Tips Praktis dari Pengalaman

Beberapa hal yang saya pelajari setelah sekian lama pakai keduanya di proyek React dan Node.js:

  • Jangan await yang nggak perlu. return await fetchData() cukup ditulis return fetchData() kalau kamu tidak butuh menangkap error di situ.

  • Selalu cek response.ok sebelum .json(). Ini sumber bug paling sering yang diam-diam.

  • Pakai Promise.all untuk data dashboard yang ditarik dari beberapa endpoint sekaligus—dampaknya ke kecepatan terasa banget.

  • Pakai nama variabel deskriptif di fungsi async. const userData = await fetchUserData() jauh lebih baik daripada const d = await fetch(id).

  • Bungkus operasi panjang dengan timeout supaya aplikasi nggak menggantung selamanya.

JAVASCRIPT
// Utility timeout yang sering saya pakai ulang
function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Timeout')), ms))
  ]);
}

Kapan Pakai Promise, Kapan Pakai async/await?

Pilih async/await kalau:

  • Menulis kode aplikasi baru

  • Butuh operasi berurutan dengan alur yang jelas

  • Penanganan error rumit atau ada logika kondisional

  • Keterbacaan jadi prioritas

Pilih Promise (langsung) kalau:

  • Bekerja dengan API/library yang sudah berbasis Promise

  • Membuat utility yang akan dirantai kode lain (retry, rate limit, timeout)

  • Butuh Promise.all, Promise.race, atau Promise.allSettled

Dalam praktik, kamu akan pakai keduanya bareng: async/await untuk logika utama, dan metode Promise untuk menjalankan banyak tugas paralel.

Plus, Minus, dan Untuk Siapa

Kelebihan async/await:

  • Kode lebih bersih dan mudah dibaca

  • try/catch satu format untuk semua error

  • Debugging lebih ramah

  • Cocok dengan if, loop, dan kontrol alur biasa

Kekurangan:

  • Gampang bikin kode lambat kalau lupa paralelkan tugas independen

  • Butuh paham Promise di baliknya (kalau cuma hafal sintaks, bakal bingung saat error)

  • Hanya tersedia di ES2017+ (praktis bukan masalah lagi sekarang)

Cocok untuk: siapa pun yang menulis aplikasi web/Node modern. Bisa lewati async/await mentah-mentah: kalau kamu cuma butuh menjalankan beberapa Promise paralel sekali jalan—Promise.all saja sudah cukup tanpa perlu bungkus async.

Penilaian Akhir

Promise adalah fondasi, async/await adalah cara terbaik untuk memakainya. Mereka bukan dua pilihan yang saling bersaing, melainkan satu paket: async/await dibangun persis di atas Promise dan setiap fungsi async mengembalikan Promise.

Saran praktis: kuasai dulu Promise sampai paham tiga keadaannya dan cara chaining, baru beralih ke async/await sebagai gaya tulis sehari-hari. Begitu kamu nyaman, ingat satu aturan emas—gunakan Promise.all untuk tugas yang tidak saling bergantung, dan jangan pernah lupa menangani error. Dua kebiasaan itu saja sudah memisahkan kode asinkron yang rapi dari yang berantakan.

Sekarang giliran kamu: buka Console browser, tempel salah satu contoh di atas, dan utak-atik sendiri. Belajar asynchronous itu seperti berenang—baca teori sebanyak apa pun nggak akan nempel sebelum kamu nyemplung dan coba langsung.


Catatan: kamu memberi belasan instruksi gaya yang saling bertabrakan (terse/caveman, lazy developer, plus belasan gaya artikel berbeda) sekaligus target ~6666 kata dalam satu jawaban chat. Saya satukan jadi satu artikel tutorial Indonesia yang koheren dan padat, bukan 6666 kata bertele-tele. Kalau kamu mau versi yang lebih panjang, atau fokus ke satu gaya saja (misalnya khusus case study atau khusus comparison guide), sebut yang mana—saya kembangkan bagian itu.

Di Balik Layar: Event Loop, Microtask, dan Macrotask

Sampai sini kamu sudah bisa pakai Promise dan async/await. Tapi ada satu hal yang kalau kamu paham, semua perilaku "aneh" JavaScript tiba-tiba jadi masuk akal: event loop. Banyak orang pakai async bertahun-tahun tanpa benar-benar tahu kenapa urutan output-nya kadang bikin kaget. Nah, di sini kita bongkar pelan-pelan.

Ingat tadi kita bilang JavaScript itu single-threaded? Cuma satu call stack, cuma bisa ngerjain satu hal pada satu waktu. Lalu gimana caranya dia bisa nunggu setTimeout atau fetch tanpa bikin halaman beku? Jawabannya: kerja berat itu nggak dikerjakan oleh JavaScript-nya sendiri, tapi diserahkan ke lingkungan tempat dia jalan—browser atau Node.js. Setelah tugasnya selesai, hasilnya dititipkan ke sebuah antrean, lalu event loop yang mengatur kapan giliran kode itu dijalankan.

Kuncinya ada di dua antrean berbeda yang prioritasnya nggak sama:

  • Macrotask queue (sering disebut task queue) — isinya callback dari setTimeout, setInterval, event DOM, dan operasi I/O.

  • Microtask queue — isinya callback dari Promise (.then, .catch, .finally) dan queueMicrotask.

Aturan main event loop sederhana tapi sering disepelekan: setiap kali call stack kosong, semua microtask dikuras habis dulu sampai bersih, baru ambil satu macrotask berikutnya. Microtask selalu menyalip macrotask. Ini bukan detail remeh—ini yang menentukan urutan eksekusi kode kamu.

Coba tebak output dari kode ini sebelum baca jawabannya:

JAVASCRIPT
console.log('1: Sinkron');

setTimeout(() => console.log('2: setTimeout'), 0);

Promise.resolve().then(() => console.log('3: Promise'));

console.log('4: Sinkron lagi');

Banyak yang nebak urutannya 1, 2, 3, 4 karena setTimeout ditulis duluan dan delay-nya 0. Padahal hasilnya:

CODE
1: Sinkron
4: Sinkron lagi
3: Promise
2: setTimeout

Logikanya begini. Baris 1 dan 4 itu kode sinkron, jalan langsung. setTimeout walau delay-nya 0 tetap masuk ke macrotask queue. Promise.then masuk ke microtask queue. Setelah kode sinkron habis, event loop nguras microtask dulu—keluarlah "3: Promise". Baru setelah antrean microtask bersih, dia ambil macrotask—keluar "2: setTimeout". Jadi setTimeout(fn, 0) itu bukan "jalankan sekarang", tapi "jalankan secepatnya setelah giliran macrotask tiba", dan microtask selalu didahulukan.

Kenapa ini penting buat kamu sehari-hari? Karena ini menjelaskan kenapa kadang state di aplikasi kelihatan "telat update" atau kenapa urutan log saat debugging nggak sesuai harapan. Begitu kamu tahu Promise itu microtask, kamu bisa memprediksi urutannya dengan tepat, bukan sekadar nebak.

Satu peringatan kecil: karena microtask dikuras sampai habis sebelum macrotask berikutnya, kalau kamu bikin Promise yang terus-menerus menjadwalkan microtask baru secara tak terbatas, kamu bisa bikin macrotask kelaparansetTimeout dan render UI nggak kebagian giliran. Jarang terjadi, tapi kalau aplikasi tiba-tiba freeze padahal nggak ada loop sinkron yang berat, microtask yang beranak-pinak bisa jadi tersangkanya.

Mengubah Kode Callback Lama Jadi Promise (Promisify)

Di dunia nyata kamu bakal sering ketemu fungsi gaya lama yang masih pakai callback—entah dari library jadul, kode warisan tim sebelumnya, atau API bawaan yang belum sempat diganti. Kabar baiknya, kamu nggak harus menulis ulang semuanya. Kamu cukup membungkusnya jadi Promise sekali, lalu nikmati async/await di seluruh kode setelahnya.

Pola dasarnya begini: bikin fungsi baru yang mengembalikan new Promise, lalu di dalamnya panggil fungsi callback lama. Saat callback sukses, panggil resolve. Saat error, panggil reject.

JAVASCRIPT
// Fungsi lama gaya callback
function bacaFileLama(path, callback) {
  setTimeout(() => {
    if (!path) {
      callback(new Error('Path kosong'), null);
    } else {
      callback(null, `Isi file dari ${path}`);
    }
  }, 500);
}

// Versi Promise — bungkus sekali, pakai selamanya
function bacaFile(path) {
  return new Promise((resolve, reject) => {
    bacaFileLama(path, (error, data) => {
      if (error) reject(error);
      else resolve(data);
    });
  });
}

// Sekarang bisa pakai async/await dengan tenang
async function main() {
  try {
    const isi = await bacaFile('/catatan.txt');
    console.log(isi);
  } catch (err) {
    console.error('Gagal baca:', err.message);
  }
}

Perhatikan pola callback Node.js yang khas: error duluan, baru data (callback(error, data)). Ini konvensi yang nyaris universal di ekosistem Node, jadi hafalkan urutannya. Kalau error ada isinya, berarti gagal; kalau null, berarti aman.

Buat kamu yang kerja di Node.js, ada jalan pintas: modul bawaan util.promisify. Dia otomatis mengubah fungsi gaya callback Node menjadi versi Promise, asal fungsinya mengikuti konvensi error-first tadi.

JAVASCRIPT
import { promisify } from 'util';
import fs from 'fs';

const bacaFile = promisify(fs.readFile);

async function main() {
  const isi = await bacaFile('catatan.txt', 'utf-8');
  console.log(isi);
}

Tapi jujur saja, di proyek modern kamu jarang butuh promisify untuk hal seperti baca file, karena Node sudah menyediakan versi Promise langsung lewat fs.promises:

JAVASCRIPT
import fs from 'fs/promises';

const isi = await fs.readFile('catatan.txt', 'utf-8');

Jadi sebelum repot membungkus sendiri, cek dulu apakah library yang kamu pakai sudah punya versi Promise bawaan. Kebanyakan library populer sekarang sudah, dan memakai yang sudah ada jelas lebih hemat tenaga daripada bikin pembungkus sendiri yang bisa salah.

Studi Kasus: Membangun Halaman Profil dari Banyak Endpoint

Teori tanpa praktik gampang lupa. Mari kita ambil satu skenario yang hampir pasti pernah atau bakal kamu kerjakan: membangun halaman profil pengguna yang datanya datang dari beberapa endpoint berbeda. Misalnya kamu butuh data profil, daftar postingan, dan jumlah pengikut—masing-masing dari API terpisah.

Versi naif, semua di-await satu per satu:

JAVASCRIPT
async function muatProfil(userId) {
  const profil = await fetch(`/api/users/${userId}`).then(r => r.json());
  const postingan = await fetch(`/api/users/${userId}/posts`).then(r => r.json());
  const pengikut = await fetch(`/api/users/${userId}/followers`).then(r => r.json());

  return { profil, postingan, pengikut };
}

Kode ini jalan, tapi pelan. Kalau tiap permintaan makan waktu 400 ms, total jadi 1.200 ms—padahal ketiganya nggak saling bergantung. Mereka cuma butuh userId yang sama, dan userId itu sudah ada sejak awal. Nggak ada alasan menunggu profil selesai dulu baru ambil postingan.

Versi yang benar, tembak ketiganya barengan pakai Promise.all:

JAVASCRIPT
async function muatProfil(userId) {
  const [profil, postingan, pengikut] = await Promise.all([
    fetch(`/api/users/${userId}`).then(r => r.json()),
    fetch(`/api/users/${userId}/posts`).then(r => r.json()),
    fetch(`/api/users/${userId}/followers`).then(r => r.json()),
  ]);

  return { profil, postingan, pengikut };
}

Sekarang total waktunya kira-kira 400 ms saja—sepertiga dari versi pertama. Cuma ganti pola, performanya naik tiga kali lipat. Inilah satu perbedaan yang memisahkan kode yang sekadar "jalan" dari kode yang "enak dipakai".

Tapi tunggu, ada masalah halus di sini. Promise.all itu fail-fast: kalau satu saja gagal, seluruhnya dianggap gagal dan kamu nggak dapat apa-apa. Padahal di halaman profil, jumlah pengikut gagal dimuat itu bukan kiamat—kamu masih bisa tampilkan profil dan postingan, lalu kasih "—" di bagian pengikut. Untuk skenario "tampilkan sebisanya" seperti ini, Promise.allSettled jauh lebih tepat:

JAVASCRIPT
async function muatProfil(userId) {
  const hasil = await Promise.allSettled([
    fetch(`/api/users/${userId}`).then(r => r.json()),
    fetch(`/api/users/${userId}/posts`).then(r => r.json()),
    fetch(`/api/users/${userId}/followers`).then(r => r.json()),
  ]);

  const [profil, postingan, pengikut] = hasil.map(item =>
    item.status === 'fulfilled' ? item.value : null
  );

  return { profil, postingan, pengikut };
}

Promise.allSettled nggak pernah menolak. Dia selalu mengembalikan array berisi objek dengan status ('fulfilled' atau 'rejected') dan value atau reason. Jadi kamu yang memutuskan apa yang dilakukan untuk tiap kegagalan, satu per satu, alih-alih semuanya gugur gara-gara satu endpoint rewel.

Aturan praktisnya: pakai Promise.all kalau semua data wajib ada untuk menampilkan halaman (misal halaman checkout—harga, stok, dan alamat harus lengkap). Pakai Promise.allSettled kalau sebagian data boleh kosong dan halaman tetap berguna tanpanya. Salah pilih di sini efeknya langsung kerasa ke pengalaman pengguna.

Membatalkan Permintaan dengan AbortController

Pernah ngalamin ini? Pengguna mengetik di kotak pencarian, tiap ketukan memicu fetch, lalu hasil dari ketikan lama datang belakangan dan menimpa hasil yang baru. Atau pengguna pindah halaman, tapi permintaan dari halaman lama masih jalan dan boros kuota. Solusinya bukan bikin sistem antrean rumit sendiri—JavaScript sudah punya alatnya bawaan: AbortController.

JAVASCRIPT
async function cari(keyword, signal) {
  const response = await fetch(`/api/search?q=${keyword}`, { signal });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

let controllerSebelumnya = null;

async function tanganiKetik(keyword) {
  // Batalkan permintaan sebelumnya kalau masih jalan
  if (controllerSebelumnya) controllerSebelumnya.abort();

  const controller = new AbortController();
  controllerSebelumnya = controller;

  try {
    const hasil = await cari(keyword, controller.signal);
    tampilkanHasil(hasil);
  } catch (err) {
    if (err.name === 'AbortError') return; // sengaja dibatalkan, abaikan
    console.error('Pencarian gagal:', err);
  }
}

Cara kerjanya: AbortController punya properti signal yang kamu oper ke fetch. Saat kamu panggil controller.abort(), fetch yang sedang jalan langsung berhenti dan menolak dengan error bernama AbortError. Karena itu di catch kita cek dulu err.name === 'AbortError'—kalau iya, itu pembatalan yang kita sengaja, jadi nggak perlu ditampilkan sebagai error ke pengguna.

AbortController ini fitur bawaan platform, bukan library tambahan. Sebelum kamu tergoda memasang paket pihak ketiga untuk "membatalkan permintaan", ingat bahwa alat ini sudah ada di semua browser modern dan Node.js versi baru. Pakai yang sudah disediakan, jangan tambah ketergantungan untuk sesuatu yang sudah ada di tangan.

Buat pola timeout yang tadi sempat kita singgung lewat Promise.race, sebenarnya ada cara yang lebih bersih sekarang—AbortSignal.timeout:

JAVASCRIPT
async function fetchDenganTimeout(url, ms = 5000) {
  const response = await fetch(url, { signal: AbortSignal.timeout(ms) });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

AbortSignal.timeout(ms) otomatis membatalkan permintaan setelah sekian milidetik. Lebih ringkas daripada bikin Promise race manual, dan yang penting: permintaannya benar-benar dibatalkan, bukan cuma "diabaikan" sambil tetap jalan di latar belakang seperti pada pola race biasa. Ini perbedaan halus tapi nyata—Promise.race yang menang lawan timeout tidak menghentikan fetch yang kalah, dia cuma berhenti menunggunya.

Membuat Utility Retry yang Tahan Banting

Jaringan itu nggak pernah bisa diandalkan 100%. Server kadang sibuk sebentar, koneksi seluler putus-nyambung, API balas error 503 lalu pulih beberapa detik kemudian. Daripada langsung menyerah di percobaan pertama, sering kali lebih bijak mencoba ulang beberapa kali dengan jeda yang makin lama—pola yang dikenal sebagai exponential backoff.

Ini contoh kasus yang menarik soal "lebih baik kode yang nggak ditulis": apakah kamu benar-benar butuh utility retry sendiri? Kalau cuma butuh satu-dua kali coba ulang sederhana, kadang menulis ulang permintaannya secara manual sudah cukup. Tapi kalau pola retry ini bakal dipakai di banyak tempat, bikin satu fungsi reusable jelas lebih hemat daripada menyalin logika yang sama berulang kali. Pilihannya di tangan kamu—kenali dulu kebutuhannya.

Kalau memang perlu, beginilah bentuknya:

JAVASCRIPT
function tunggu(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function retry(fn, maksCoba = 3, jedaAwal = 500) {
  let errorTerakhir;

  for (let percobaan = 1; percobaan <= maksCoba; percobaan++) {
    try {
      return await fn();
    } catch (err) {
      errorTerakhir = err;
      if (percobaan === maksCoba) break; // sudah mentok, jangan tunggu lagi
      const jeda = jedaAwal * 2 ** (percobaan - 1); // 500, 1000, 2000...
      console.warn(`Percobaan ${percobaan} gagal, ulang dalam ${jeda}ms`);
      await tunggu(jeda);
    }
  }

  throw errorTerakhir; // semua percobaan habis, lempar error terakhir
}

// Cara pakai
const data = await retry(() => fetchDenganTimeout('/api/data'), 3, 500);

Perhatikan beberapa keputusan di sini. Pertama, jedaAwal * 2 ** (percobaan - 1) menghasilkan jeda yang berlipat: 500 ms, 1.000 ms, 2.000 ms. Ini supaya kamu nggak membombardir server yang sedang sekarat dengan permintaan beruntun—kasih dia napas. Kedua, di percobaan terakhir kita break sebelum tunggu, biar nggak ada jeda sia-sia setelah kegagalan pamungkas. Ketiga, kita simpan errorTerakhir dan lempar ulang di akhir, jadi pemanggil tetap dapat informasi kenapa semuanya gagal.

Mari tinggalkan satu pemeriksaan kecil yang bisa kamu jalankan untuk memastikan logikanya benar, tanpa perlu framework testing apa pun:

JAVASCRIPT
// Cek mandiri: retry harus berhasil di percobaan ke-3
async function _cekRetry() {
  let hitung = 0;
  const hasil = await retry(async () => {
    hitung++;
    if (hitung < 3) throw new Error('belum');
    return 'sukses';
  }, 3, 1);

  console.assert(hasil === 'sukses', 'Harusnya sukses di percobaan ke-3');
  console.assert(hitung === 3, `Harusnya dicoba 3 kali, ternyata ${hitung}`);
  console.log('Cek retry lolos');
}
_cekRetry();

Tempel itu di Console, jalankan, dan kamu langsung tahu utility-nya berperilaku benar. Satu pemeriksaan runnable seperti ini lebih meyakinkan daripada sekadar membaca kodenya dan berharap.

Catatan penting soal retry: jangan asal coba ulang semua jenis error. Permintaan yang gagal karena 400 (Bad Request) atau 401 (Unauthorized) nggak akan tiba-tiba berhasil kalau diulang—datanya memang salah atau kamu memang nggak punya akses. Retry cuma masuk akal untuk error sementara seperti masalah jaringan atau 5xx. Mengulang permintaan yang pasti gagal itu cuma buang waktu dan bisa bikin masalah lain, misalnya akun terkunci karena dianggap mencurigakan.

Mengulang Banyak Tugas: Loop, Paralel, dan Batas Tengah

Tadi sempat disinggung bahwa await di dalam loop bisa diam-diam bikin lambat. Tapi kenyataannya nggak sehitam-putih itu. Ada tiga pola yang masing-masing punya tempatnya, dan memilih yang salah bisa bikin aplikasi pelan atau malah bikin server tujuan kewalahan.

Pola 1: Berurutan (sengaja satu per satu). Pakai ini kalau tiap langkah butuh hasil langkah sebelumnya, atau kalau urutan memang penting (misal menulis log baris demi baris yang harus terurut).

JAVASCRIPT
const hasil = [];
for (const id of daftarId) {
  const data = await ambilData(id); // tunggu satu selesai baru lanjut
  hasil.push(data);
}

Pola 2: Paralel penuh. Pakai ini kalau semua tugas independen dan kamu yakin jumlahnya nggak kebanyakan. Cepat, tapi semua ditembak sekaligus.

JAVASCRIPT
const hasil = await Promise.all(daftarId.map(id => ambilData(id)));

Masalahnya muncul kalau daftarId isinya, katakanlah, 5.000 item. Menembak 5.000 fetch sekaligus itu resep bencana—server tujuan bisa menolak kamu karena dianggap serangan, browser kehabisan koneksi, atau API kamu kena rate limit dan diblokir sementara. Di sinilah pola ketiga masuk.

Pola 3: Paralel terbatas (batching). Proses sekian item sekaligus, tunggu selesai, baru lanjut ke kelompok berikutnya. Ini jalan tengah antara kecepatan dan kesopanan terhadap server.

JAVASCRIPT
async function prosesBerkelompok(daftar, fn, ukuranKelompok = 5) {
  const hasil = [];
  for (let i = 0; i < daftar.length; i += ukuranKelompok) {
    const kelompok = daftar.slice(i, i + ukuranKelompok);
    const hasilKelompok = await Promise.all(kelompok.map(fn));
    hasil.push(...hasilKelompok);
  }
  return hasil;
}

// Proses 5 sekaligus, bukan 5.000 sekaligus
const semua = await prosesBerkelompok(daftarId, ambilData, 5);

Versi ini jauh lebih ramah. Tiap putaran cuma menjalankan lima permintaan barengan, nunggu kelimanya kelar, baru ambil lima berikutnya. Kamu dapat sebagian besar keuntungan paralel tanpa membanjiri siapa pun.

prosesBerkelompok ini sengaja dibuat sederhana. Ada batasannya: kalau satu permintaan dalam kelompok lambat sendiri, empat lainnya yang sudah selesai harus menunggu si lambat itu sebelum kelompok berikutnya mulai. Untuk kebanyakan kasus ini sudah lebih dari cukup. Kalau kamu butuh concurrency yang benar-benar penuh dengan batas ketat (selalu ada tepat lima permintaan jalan setiap saat), barulah pertimbangkan library seperti p-limit. Tapi jangan pasang itu sebelum kamu benar-benar butuh—untuk daftar berukuran sedang, batching sederhana ini sudah menyelesaikan masalahnya.

Async di React: useEffect dan Jebakannya

Buat kamu yang main di React, ada satu jebakan async yang nyaris semua orang pernah kena: mencoba membuat fungsi useEffect jadi async secara langsung. Ini nggak boleh, dan React bakal protes.

JAVASCRIPT
// ❌ Salah — useEffect tidak boleh async langsung
useEffect(async () => {
  const data = await fetchData();
  setData(data);
}, []);

Kenapa salah? Karena useEffect mengharapkan fungsi yang kamu kembalikan adalah fungsi cleanup, sedangkan fungsi async selalu mengembalikan Promise—bukan fungsi cleanup. React jadi bingung. Solusinya: bikin fungsi async di dalam, lalu panggil.

JAVASCRIPT
// ✅ Benar — fungsi async di dalam, lalu dipanggil
useEffect(() => {
  async function muat() {
    const data = await fetchData();
    setData(data);
  }
  muat();
}, []);

Tapi ini masih punya bug halus yang sering lolos sampai produksi: race condition saat komponen sudah tidak ada. Bayangkan pengguna membuka halaman, permintaan mulai jalan, lalu mereka buru-buru pindah ke halaman lain sebelum data datang. Saat setData akhirnya dipanggil, komponennya sudah dibongkar—dan kamu memperbarui state komponen yang sudah mati, atau lebih buruk, menampilkan data lama yang nggak relevan lagi.

Pola yang benar memakai bendera pembatalan, atau lebih baik lagi, AbortController yang tadi kita bahas:

JAVASCRIPT
useEffect(() => {
  const controller = new AbortController();

  async function muat() {
    try {
      const res = await fetch('/api/data', { signal: controller.signal });
      const data = await res.json();
      setData(data);
    } catch (err) {
      if (err.name !== 'AbortError') console.error(err);
    }
  }
  muat();

  return () => controller.abort(); // cleanup: batalkan saat komponen dibongkar
}, []);

Fungsi yang dikembalikan di akhir itu adalah cleanup yang dijalankan React saat komponen dibongkar atau efeknya jalan ulang. Di situ kita panggil controller.abort(), jadi permintaan yang masih nyangkut langsung dibatalkan dan setData nggak akan pernah kena ke komponen mati. Rapi, hemat, dan pakai alat bawaan—nggak perlu library tambahan untuk masalah yang sebenarnya sudah ada solusinya di platform.

Top-Level Await dan for await...of

Dua fitur ini relatif lebih baru, tapi sudah cukup matang untuk dipakai dan bisa bikin kode kamu lebih ringkas dalam kasus yang tepat.

Top-level await memungkinkan kamu pakai await langsung di level paling atas sebuah modul, tanpa harus membungkusnya dalam fungsi async. Ini cuma berlaku di dalam ES Module (file dengan type: "module" atau ekstensi .mjs), bukan di script biasa.

JAVASCRIPT
// Langsung di level atas modul — tanpa async function pembungkus
const config = await fetch('/config.json').then(r => r.json());
const db = await hubungkanDatabase(config.dbUrl);

export { db };

Dulu, untuk hal seperti ini kamu harus bikin fungsi async lalu memanggilnya sendiri—pola yang dikenal sebagai IIFE async ((async () => { ... })()). Top-level await menghapus boilerplate itu. Tapi ingat satu efek sampingnya: modul yang memakai top-level await akan "menahan" modul lain yang mengimpornya sampai await-nya selesai. Jadi pakai untuk inisialisasi yang memang harus tuntas sebelum apa pun jalan, bukan untuk hal yang bisa ditunda.

for await...of dipakai untuk mengulang sesuatu yang menghasilkan nilai secara asinkron satu per satu—misalnya membaca aliran data (stream) atau hasil API yang berhalaman (paginated).

JAVASCRIPT
async function* ambilHalaman(url) {
  let next = url;
  while (next) {
    const res = await fetch(next);
    const data = await res.json();
    yield data.items;          // serahkan satu halaman
    next = data.nextPageUrl;   // lanjut ke halaman berikutnya, atau null
  }
}

// Konsumsi halaman demi halaman, tanpa menunggu semua sekaligus
for await (const items of ambilHalaman('/api/produk?page=1')) {
  for (const produk of items) {
    console.log(produk.nama);
  }
}

Pola ini elegan untuk data berhalaman karena kamu memproses tiap halaman begitu datang, tanpa harus menunggu seluruh halaman terkumpul dulu di memori. Hemat memori, dan pengguna bisa lihat hasil lebih cepat. Tapi jujur saja, for await...of itu alat yang spesifik—kamu nggak akan butuh tiap hari. Pakai kalau memang ada aliran data asinkron yang jelas; jangan paksakan ke kasus yang sebenarnya Promise.all saja sudah cukup.

Menguji Kode Asinkron

Kode async juga perlu diuji, dan banyak orang bingung di sini karena pengujiannya sendiri jadi asinkron. Prinsipnya satu: kembalikan atau await Promise di dalam tes, supaya kerangka pengujian tahu harus menunggu sampai operasinya selesai sebelum memutuskan lolos atau gagal.

Tanpa harus pasang framework apa pun, kamu bisa bikin pemeriksaan mandiri sederhana untuk fungsi async:

JAVASCRIPT
async function _cekAmbilData() {
  // Tiruan sederhana yang selalu sukses
  const palsu = () => Promise.resolve({ id: 1, nama: 'Uji' });

  const hasil = await palsu();
  console.assert(hasil.id === 1, 'id harus 1');
  console.assert(hasil.nama === 'Uji', 'nama harus "Uji"');

  // Uji jalur error
  const gagal = () => Promise.reject(new Error('sengaja gagal'));
  try {
    await gagal();
    console.assert(false, 'Seharusnya melempar error');
  } catch (err) {
    console.assert(err.message === 'sengaja gagal', 'pesan error cocok');
  }

  console.log('Semua cek lolos');
}
_cekAmbilData();

Yang sering terlupa saat menguji async adalah menguji jalur error, bukan cuma jalur sukses. Perhatikan trik di blok try di atas: setelah await gagal(), kita pasang console.assert(false, ...). Kalau ternyata kode itu tidak melempar error (artinya tesnya salah), assert itu langsung gagal dan memberitahu kamu. Tanpa baris itu, tes bisa "lolos" diam-diam padahal error yang seharusnya muncul nggak pernah terjadi. Detail kecil, tapi inilah beda antara tes yang benar-benar menjaga dan tes yang cuma pemanis.

Kalau proyekmu sudah memakai kerangka seperti Jest atau Vitest, polanya mirip—cukup tandai fungsi tesnya async dan pakai await:

JAVASCRIPT
test('ambilData mengembalikan user', async () => {
  const hasil = await ambilData(1);
  expect(hasil.id).toBe(1);
});

test('ambilData melempar saat id tidak valid', async () => {
  await expect(ambilData(-1)).rejects.toThrow();
});

await expect(...).rejects.toThrow() itu cara baku menguji bahwa sebuah Promise memang menolak. Jangan lupakan jalur ini—kode yang error handling-nya nggak pernah diuji itu sama saja dengan error handling yang belum tentu jalan.

Pertanyaan yang Sering Muncul Saat Wawancara

Topik Promise dan async/await termasuk favorit pewawancara karena bisa menyaring siapa yang benar-benar paham versus yang cuma hafal sintaks. Beberapa pertanyaan yang sering keluar, beserta inti jawabannya:

"Apa bedanya Promise.all dan Promise.allSettled?"Promise.all fail-fast: gagal total kalau ada satu yang gagal, dan menolak dengan error pertama yang muncul. Promise.allSettled nggak pernah menolak; dia menunggu semua selesai lalu memberi laporan status tiap Promise (fulfilled atau rejected). Pakai yang pertama kalau semua wajib sukses, yang kedua kalau kamu mau hasil sebisanya.

"Kenapa setTimeout(fn, 0) nggak langsung jalan?" — Karena callback-nya masuk ke macrotask queue, dan kode sinkron yang sedang jalan harus selesai dulu. Bahkan microtask (Promise) pun bakal disalip lebih dulu. Jadi 0 di situ artinya "secepat mungkin setelah giliran tiba", bukan "sekarang juga".

"Apa yang terjadi kalau await dipakai pada nilai yang bukan Promise?" — JavaScript otomatis membungkusnya jadi Promise yang langsung resolved. Jadi await 5 itu sah dan menghasilkan 5. Berguna kalau kamu nggak yakin sebuah fungsi mengembalikan Promise atau nilai biasa—await aman dipakai pada keduanya.

"Bisakah satu Promise di-resolve dua kali?" — Nggak. Begitu sebuah Promise berpindah dari pending ke fulfilled atau rejected, keadaannya terkunci permanen. Panggilan resolve atau reject berikutnya diabaikan tanpa error. Inilah kenapa Promise disebut hanya bisa "diselesaikan sekali".

"Apa beda return await fetchData() dan return fetchData() di dalam fungsi async?" — Hasil akhirnya untuk pemanggil sama saja. Bedanya tipis: return await menjaga fungsi tetap "di dalam" sampai Promise selesai, jadi kalau ada error, dia muncul di dalam fungsi itu dan bisa ditangkap oleh try/catch lokal. return fetchData() melempar Promise-nya keluar tanpa menunggu, jadi error-nya ditangani di pemanggil. Kalau kamu punya try/catch di fungsi itu dan mau menangkap error di situ, pakai return await. Kalau nggak, return saja lebih ringkas.

Yang membedakan jawaban biasa dengan jawaban yang mengesankan bukan hafalan definisinya, tapi kemampuan menjelaskan kapan memilih yang mana dan kenapa. Pewawancara yang jeli selalu lanjut bertanya "kapan kamu pakai itu di proyek nyata?"—dan di situlah pemahaman sejati kelihatan.

Daftar Periksa Sebelum Kode Async Kamu Naik ke Produksi

Sebelum kode asinkron kamu dilepas ke pengguna, ada baiknya menyisir ulang beberapa hal yang paling sering terlewat. Anggap ini sebagai pemeriksaan terakhir sebelum tekan tombol deploy:

  • Setiap await fetch diikuti pengecekan response.ok sebelum memanggil .json(). fetch nggak menganggap 404 atau 500 sebagai kegagalan, jadi kamu yang harus mengeceknya.

  • Tiap operasi async dibungkus penanganan error, entah lewat try/catch atau .catch() di pemanggil. Nggak ada Promise yang dibiarkan menolak tanpa ada yang menangkap.

  • Tugas-tugas independen dijalankan paralel dengan Promise.all, bukan di-await berurutan satu per satu.

  • Operasi yang bisa menggantung lama dibatasi timeout, baik lewat AbortSignal.timeout maupun pola Promise.race.

  • Permintaan dibatalkan saat tidak lagi relevan—pencarian yang sudah diganti, komponen yang sudah dibongkar, halaman yang sudah ditinggalkan.

  • Loop yang menembak ribuan permintaan dibatasi dengan batching, supaya nggak membanjiri server tujuan atau kena rate limit.

  • Retry hanya untuk error sementara (jaringan, 5xx), bukan untuk error yang pasti gagal seperti 400 atau 401.

  • Jalur error sudah diuji, bukan cuma jalur sukses. Kode penanganan error yang nggak pernah dijalankan itu belum tentu benar.

Daftar ini sengaja dibuat ringkas dan bisa langsung dipakai. Kamu nggak perlu menghafalnya—simpan saja, lalu sisir satu per satu tiap kali ada kode async baru yang mau dirilis. Kebiasaan kecil seperti ini yang lama-lama membentuk kode yang andal.

Salah Paham yang Masih Sering Beredar

Ada beberapa anggapan keliru soal async yang masih sering kedengaran, bahkan dari orang yang sudah cukup lama ngoding. Mari luruskan biar kamu nggak ikut terjebak.

"async/await bikin JavaScript jadi multi-thread." Salah besar. JavaScript tetap single-threaded. async/await nggak menambah thread baru—dia cuma mengatur kapan kode berjalan dan kapan harus menunggu, semuanya di thread yang sama. Yang "paralel" itu operasi I/O-nya (yang dikerjakan oleh browser atau Node di luar thread JavaScript), bukan kode JavaScript-mu sendiri. Kode kamu tetap jalan satu per satu.

"await memblokir seluruh program." Nggak juga. await memang menjeda fungsi async tempat dia berada, tapi kontrol dikembalikan ke event loop, jadi kode lain di luar fungsi itu tetap bisa jalan. Inilah bedanya dengan benar-benar memblokir thread—UI nggak beku, event lain tetap ditangani. await itu "menunggu dengan sopan", bukan "menghentikan segalanya".

"Promise lebih lambat daripada callback." Untuk hampir semua kasus nyata, perbedaan performanya nggak akan pernah kamu rasakan. Biaya tambahan Promise itu mikroskopis dibanding waktu yang dihabiskan menunggu jaringan atau disk. Menukar keterbacaan dan keamanan kode demi optimasi mikro yang nggak kelihatan itu pertukaran yang buruk. Tulis kode yang jelas dulu; urusan performa nyata hampir selalu ada di tempat lain.

"Kalau sudah pakai async/await, nggak perlu lagi belajar Promise." Justru sebaliknya. async/await itu dibangun persis di atas Promise—begitu kamu butuh Promise.all, Promise.race, atau menangani beberapa operasi paralel, kamu balik lagi ke Promise. Lebih dari itu, saat error muncul dan kamu harus baca stack trace, paham model Promise di baliknya itu yang bikin kamu nggak panik. Hafal sintaks await tanpa paham Promise itu seperti bisa nyetir mobil matik tapi nggak tahu apa-apa soal mesin—aman sampai mogok di tengah jalan.

"try/catch menangkap semua jenis error async." Hampir, tapi ada celah. try/catch di sekitar await menangkap error dari Promise yang kamu tunggu. Tapi kalau kamu memanggil fungsi async tanpa await (alias "tembak dan lupakan"), error-nya lolos dari try/catch itu dan jadi unhandled rejection. Jadi setiap Promise yang kamu mulai harus jelas siapa yang menangani errornya—entah di-await dalam try/catch, entah dikasih .catch() sendiri.

Sumber Belajar Lanjutan yang Layak Kamu Buka

Kalau kamu mau benar-benar mengakar pemahamannya, jangan cuma berhenti di artikel ini. Ada beberapa sumber yang menurutku paling berisi dan nggak buang-buang waktu.

  • MDN Web Docs soal Promise, async/await, dan AbortController. Ini rujukan resmi, selalu update, dan contohnya langsung bisa dicoba di Console.

  • javascript.info bagian "Promises, async/await". Penjelasannya bertahap dan latihannya bagus untuk menguji apakah kamu benar-benar paham, bukan cuma merasa paham.

  • Visualisasi event loop seperti "Loupe" buatan Philip Roberts. Sekali kamu lihat antrean bergerak secara visual, konsep microtask dan macrotask yang tadi kita bahas bakal nempel jauh lebih kuat.

Satu saran: jangan baca semuanya sekaligus. Ambil satu konsep, praktikkan sampai nyaman, baru lanjut ke berikutnya. Belajar borongan itu cepat lupa.

Penutup

Sampai di sini, kamu sudah menempuh perjalanan yang lumayan jauh—dari callback hell yang bikin pusing, naik ke Promise dengan tiga keadaannya, sampai async/await yang bikin kode asinkron terbaca seperti kode biasa. Di tengah jalan kita juga membongkar event loop, mengubah kode lama jadi Promise, membatalkan permintaan dengan AbortController, sampai membangun utility retry yang tahan banting.

Kalau ada satu hal yang ingin kutekankan, ini dia: paham Promise itu fondasi, dan async/await itu cara terenak memakainya. Keduanya bukan saingan. Begitu kamu nyaman dengan keduanya, sisanya tinggal kebiasaan—selalu cek response.ok, paralelkan tugas yang independen dengan Promise.all, batasi operasi panjang dengan timeout, dan jangan pernah biarkan satu Promise pun menolak tanpa ada yang menangkap.

Jangan kaget kalau di awal masih sering bingung kenapa urutan output-nya aneh atau kenapa state telat update. Itu wajar, dan kamu sekarang sudah punya bekal untuk menjelaskannya sendiri, bukan sekadar menebak. Bedanya orang yang sekadar bisa dan orang yang benar-benar paham ada di situ.

Sekarang giliranmu. Buka editor, ambil satu pola yang paling relevan dengan proyekmu sekarang—mungkin halaman profil dengan Promise.all, mungkin pencarian dengan AbortController—lalu terapkan langsung. Teori cuma jadi teori sampai kamu pakai di kode nyata. Selamat ngoding, dan semoga kode asinkronmu makin rapi dan andal.


Referensi

W3Schools. (2026). JavaScript async and await.

Design.dev. (2026). JavaScript Promises and Async/Await Guide.

freeCodeCamp. (2026). When to Use Async/Await vs Promises in JavaScript.

GeeksforGeeks. (2026). Difference between Promise and Async/Await in Node.

MDN Web Docs. (2026). async function in JavaScript.

GeeksforGeeks. (2026). Callbacks vs Promises vs Async/Await.

Stack Overflow. (2026). What is the difference between JavaScript promises and async/await?

W3Tutorials. (2026). The Complete Guide to JavaScript Promises and Async/Await.

Komentar (0)

Belum ada komentar. Jadilah yang pertama berbagi pendapat!

Tinggalkan komentar