JavaScript menjalankan kode secara single-threaded sekaligus non-blocking — ia hanya punya satu jalur eksekusi, tetapi tidak berhenti menunggu operasi yang lambat (seperti mengambil data dari server atau membaca file) selesai sebelum melanjutkan ke baris berikutnya. Konsep inilah yang disebut asynchronous programming, dan menguasainya adalah kunci untuk membangun aplikasi web yang responsif. Jika kamu baru memulai, pastikan dulu memahami dasarnya lewat artikel Memahami Dasar JavaScript: Variabel, Fungsi, dan DOM. Di tutorial ini kita akan menelusuri perjalanan JavaScript dalam menangani kode asynchronous: dari callback, Promise, hingga async/await, lalu mempraktikkannya dengan fetch API.

Kenapa JavaScript Butuh Kode Asynchronous?

Bayangkan aplikasimu mengambil data dari API yang butuh waktu 2 detik. Jika JavaScript harus menunggu sampai selesai, seluruh halaman akan membeku selama 2 detik itu. Karena itulah JavaScript memakai model non-blocking: operasi lambat dijalankan di latar belakang, sementara kode lainnya terus berjalan. Contoh paling sederhananya adalah setTimeout.

javascript
console.log("Mulai");

setTimeout(() => {
  console.log("Ini dijalankan setelah 2 detik");
}, 2000);

console.log("Selesai");

// Output:
// Mulai
// Selesai
// Ini dijalankan setelah 2 detik

Perhatikan bahwa "Selesai" dicetak lebih dulu daripada pesan di dalam setTimeout. Ini membuktikan JavaScript tidak menunggu — ia mencatat tugasnya, lalu lanjut ke baris berikutnya.

Callback: Cara Lama yang Cepat Jadi Rumit

Sebelum Promise ada, cara standar menangani asynchronous adalah callback, yaitu sebuah fungsi yang dilempar sebagai argumen dan dipanggil setelah operasi selesai.

javascript
function ambilData(callback) {
  setTimeout(() => {
    callback({ id: 1, nama: "Budi" });
  }, 1000);
}

ambilData((data) => {
  console.log(data.nama); // "Budi"
});

Masalah muncul ketika beberapa operasi harus berjalan berurutan. Callback yang bersarang menghasilkan kode yang susah dibaca — fenomena yang terkenal dengan sebutan callback hell atau pyramid of doom.

javascript
ambilUser(1, (user) => {
  ambilPostingan(user.id, (posts) => {
    ambilKomentar(posts[0].id, (comments) => {
      ambilBalasan(comments[0].id, (replies) => {
        // ... semakin dalam, semakin sulit dibaca
        console.log(replies);
      });
    });
  });
});

Mengenal Promise

Promise adalah objek yang merepresentasikan hasil dari operasi asynchronous yang mungkin berhasil atau gagal di masa depan. Ia hadir untuk merapikan callback hell. Sebuah Promise punya tiga status: pending (menunggu), fulfilled (berhasil), dan rejected (gagal).

javascript
const janji = new Promise((resolve, reject) => {
  const sukses = true;

  setTimeout(() => {
    if (sukses) {
      resolve("Data berhasil diambil");
    } else {
      reject("Terjadi kesalahan");
    }
  }, 1000);
});

janji
  .then((hasil) => console.log(hasil))   // dijalankan saat resolve()
  .catch((error) => console.error(error)); // dijalankan saat reject()

Promise selalu berakhir di salah satu dari tiga kondisi: pending (menunggu), fulfilled (berhasil, memanggil resolve), atau rejected (gagal, memanggil reject). Sekali berubah ke fulfilled atau rejected, statusnya terkunci dan tidak bisa berubah lagi — kondisi ini disebut settled.

Chaining Promise

Kekuatan Promise terlihat saat kamu merangkai beberapa operasi. Setiap .then() bisa mengembalikan nilai baru (atau Promise baru) yang diteruskan ke .then() berikutnya.

javascript
function ambilUser(id) {
  return fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
    .then((res) => res.json());
}

ambilUser(1)
  .then((user) => {
    console.log(user.name); // "Leanne Graham"
    return user.id;
  })
  .then((id) => console.log(`ID: ${id}`))
  .catch((err) => console.error("Gagal:", err));

Async/Await: Sintaks Modern yang Lebih Bersih

async/await adalah syntactic sugar di atas Promise — ia tidak menggantikan Promise, melainkan membuatnya lebih mudah ditulis dan dibaca. Cukup tandai fungsi dengan kata kunci async, lalu gunakan await untuk "menunggu" sebuah Promise selesai.

javascript
async function ambilUser(id) {
  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  const user = await res.json();
  return user;
}

ambilUser(1).then((user) => console.log(user.name));

Bandingkan dengan versi chaining di atas — async/await terlihat seperti kode sinkron biasa, jauh lebih mudah dipahami saat alur logikanya panjang.

Menangani Error dengan try/catch

Dengan async/await, penanganan error memakai blok try/catch yang familiar. Ini jauh lebih rapi dibanding menambahkan .catch() di setiap rantai.

javascript
async function ambilUser(id) {
  try {
    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);

    if (!res.ok) {
      throw new Error(`HTTP error! status: ${res.status}`);
    }

    const user = await res.json();
    console.log(user.name);
  } catch (error) {
    console.error("Terjadi kesalahan:", error.message);
  }
}

ambilUser(9999); // memicu error 404 yang tertangani rapi

Selalu periksa res.ok setelah fetch. fetch tidak melempar error untuk status HTTP seperti 404 atau 500 — ia hanya reject saat terjadi kegagalan jaringan (misalnya koneksi putus). Memeriksa res.ok mencegah bug yang sulit dilacak.

Mengambil Data dari API dengan fetch

Contoh nyata yang paling sering kamu temui adalah mengambil data dari REST API. Kita pakai JSONPlaceholder, sebuah API publik gratis untuk latihan.

javascript
async function tampilkanPostingan() {
  try {
    const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5");
    if (!res.ok) throw new Error(`HTTP ${res.status}`);

    const posts = await res.json();
    posts.forEach((post) => {
      console.log(`${post.id}. ${post.title}`);
    });
  } catch (err) {
    console.error("Gagal mengambil data:", err.message);
  }
}

tampilkanPostingan();

Untuk mengirim data, kamu tinggal menambahkan opsi method, headers, dan body pada argumen kedua fetch.

javascript
async function buatPostingan() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      title: "Belajar Async/Await",
      body: "Isi artikel",
      userId: 1,
    }),
  });

  const data = await res.json();
  console.log("Postingan baru:", data);
}

buatPostingan();

Menjalankan Banyak Operasi Secara Paralel dengan Promise.all

Jika kamu perlu mengambil beberapa data yang tidak saling bergantung, jangan menunggu satu per satu — jalankan semuanya bersamaan dengan Promise.all. Total waktunya setara operasi terlama, bukan jumlah seluruh operasi.

javascript
async function ambilSekaligus() {
  const [user, posts] = await Promise.all([
    fetch("https://jsonplaceholder.typicode.com/users/1").then((r) => r.json()),
    fetch("https://jsonplaceholder.typicode.com/posts?userId=1").then((r) => r.json()),
  ]);

  console.log("User:", user.name);
  console.log("Jumlah postingan:", posts.length);
}

ambilSekaligus();

Selain Promise.all, ada juga Promise.allSettled (menunggu semua selesai, berhasil maupun gagal) dan Promise.race (mengembalikan hasil yang paling cepat). Pilih sesuai kebutuhanmu.

Kesalahan Umum yang Harus Dihindari

Beberapa jebakan yang sering menjebak pemula: (1) lupa menulis await sehingga yang diterima adalah objek Promise, bukan nilainya; (2) memanggil async/await di top-level tanpa membungkusnya — solusinya pakai IIFE atau top-level await di modul ES; (3) menjalankan await berurutan di dalam loop padahal operasinya independen dan bisa dijalankan paralel dengan Promise.all.

Melupakan await adalah kesalahan paling umum. Tanpa await, variabel yang kamu terima adalah objek Promise berstatus pending, bukan nilainya. Selalu pastikan kamu menulis await sebelum memanggil fungsi yang mengembalikan Promise.

Menguasai asynchronous programming membuka pintu ke banyak topik lanjutan. Setelah nyaman dengan Promise dan async/await, kamu bisa membangun REST API sendiri dengan Node.js dan Express, atau memperkuat JavaScript dengan sistem tipe statis lewat TypeScript. Selamat mencoba, dan jangan lupa: selalu tangani error dengan baik!