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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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!