Pertama kali bikin fetch request di JavaScript, saya langsung kena callback hell. Bayangkan lima nested callback yang susah dibaca, susah di-debug, dan bikin pusing tujuh keliling. Kalau kamu pernah mengalami hal yang sama, tenang -- ada cara yang jauh lebih clean.
Promise dan async/await mengubah cara kita menulis kode asynchronous di JavaScript. Dulu kita harus mengandalkan callback yang berlapis-lapis, sekarang kode asynchronous bisa ditulis seolah-olah synchronous. Artikel ini bakal bahas tuntas cara pakai Promise dan async/await untuk handle HTTP request dengan benar, termasuk error handling yang sering dilupakan.
Hampir semua aplikasi web modern butuh komunikasi dengan server -- ambil data dari API, kirim form, upload file. Semua operasi ini butuh waktu, dan JavaScript tidak bisa menunggu. Makanya kode asynchronous itu fundamental banget.
Dengan async/await, kode yang dulunya nested callback seperti ini:
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
console.log(c);
});
});
});
Bisa ditulis lebih rapi:
const a = await getData();
const b = await getMoreData(a);
const c = await getEvenMoreData(b);
console.log(c);
Perbedaannya signifikan -- kode jadi lebih mudah dibaca, di-debug, dan di-maintain.
Promise adalah objek yang merepresentasikan hasil operasi asynchronous. Dia punya tiga state:
Bikin Promise itu gampang:
const myPromise = new Promise((resolve, reject) => {
// Simulasi operasi async
setTimeout(() => {
const data = { id: 1, name: 'Budi' };
resolve(data); // Berhasil
// reject(new Error('Gagal')); // Gagal
}, 1000);
});
myPromise
.then(data => console.log(data))
.catch(err => console.error(err));
Kalau kamu pernah pakai fetch(), sebenarnya kamu sudah pakai Promise. Fungsi fetch() mengembalikan Promise yang resolve ke objek Response.
Ini contoh klasik fetch dengan .then() chain:
fetch('https://api.example.com/users')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Fetch gagal:', error);
});
Masih oke untuk satu request. Tapi coba bayangkan kalau ada tiga request berurutan yang bergantung satu sama lain -- mulai deh nested .then() yang bikin pusing.
Async/await adalah syntactic sugar di atas Promise. Kamu tetap pakai Promise, tapi penulisannya lebih natural:
async function getUsers() {
try {
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error('Gagal ambil data:', error);
throw error;
}
}
Beberapa hal yang perlu diingat:
async sebelum bisa pakai awaitawait hanya bisa dipakai di dalam fungsi asyncawait menghentikan eksekusi baris itu sampai Promise selesaicatch dengan try/catch biasa, bukan .catch()Ini kekuatan async/await yang sesungguhnya. Request berurutan yang bergantung data sebelumnya:
async function getUserWithPosts(userId) {
try {
// Ambil data user
const userRes = await fetch(`https://api.example.com/users/${userId}`);
if (!userRes.ok) throw new Error('User tidak ditemukan');
const user = await userRes.json();
// Ambil postingan user
const postsRes = await fetch(`https://api.example.com/users/${userId}/posts`);
if (!postsRes.ok) throw new Error('Gagal ambil postingan');
const posts = await postsRes.json();
// Ambil komentar dari postingan pertama
const commentsRes = await fetch(`https://api.example.com/posts/${posts[0].id}/comments`);
const comments = await commentsRes.json();
return { user, posts, comments };
} catch (error) {
console.error('Error:', error.message);
throw error;
}
}
Kalau pakai .then() chain, kode di atas bakal tiga kali lebih panjang dan susah diikuti.
Tidak semua request harus berurutan. Kalau request-nya independen, jalankan secara paralel dengan Promise.all():
async function getDashboardData() {
try {
// Jalankan paralel -- jauh lebih cepat
const [users, posts, stats] = await Promise.all([
fetch('https://api.example.com/users').then(r => r.json()),
fetch('https://api.example.com/posts').then(r => r.json()),
fetch('https://api.example.com/stats').then(r => r.json())
]);
console.log('Users:', users.length);
console.log('Posts:', posts.length);
console.log('Stats:', stats);
} catch (error) {
console.error('Salah satu request gagal:', error);
}
}
Promise.all() menjalankan semua request sekaligus dan menunggu sampai semuanya selesai. Kalau salah satu gagal, seluruh promise reject. Ini cocok untuk dashboard yang butuh data dari beberapa endpoint.
Ada juga Promise.allSettled() yang tidak berhenti kalau ada yang gagal:
async function fetchAllData() {
const results = await Promise.allSettled([
fetch('https://api.example.com/users').then(r => r.json()),
fetch('https://api.example.com/posts').then(r => r.json()),
fetch('https://api.example.com/broken-endpoint').then(r => r.json())
]);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`Request ${i} berhasil:`, result.value);
} else {
console.log(`Request ${i} gagal:`, result.reason.message);
}
});
}
Cocok untuk situasi di mana kamu mau ambil semua data yang bisa diambil, meskipun ada yang error.
Banyak developer yang skip error handling atau pakai cara yang kurang tepat. Ini pola yang recommended:
class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
// Cek content-type sebelum parse JSON
const contentType = response.headers.get('content-type');
if (!response.ok) {
const errorBody = contentType?.includes('json')
? await response.json()
: await response.text();
throw {
status: response.status,
statusText: response.statusText,
body: errorBody
};
}
if (contentType?.includes('json')) {
return await response.json();
}
return await response.text();
} catch (error) {
// Network error (no internet, DNS fail, timeout)
if (error instanceof TypeError && error.message === 'Failed to fetch') {
throw new Error('Tidak bisa terhubung ke server. Cek koneksi internet kamu.');
}
throw error;
}
}
}
// Penggunaan
const api = new ApiClient('https://api.example.com');
async function main() {
try {
const users = await api.request('/users');
console.log(users);
} catch (error) {
if (error.status === 401) {
console.error('Token expired, login ulang');
} else if (error.status === 404) {
console.error('Data tidak ditemukan');
} else if (error.status >= 500) {
console.error('Server error, coba lagi nanti');
} else {
console.error('Error:', error);
}
}
}
Pol penting: selalu cek response.ok sebelum parse JSON. fetch() tidak otomatis throw error untuk status 4xx atau 5xx -- dia hanya reject kalau ada network error.
Di production, request kadang gagal karena timeout atau server sibuk. Pattern retry sangat berguna:
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return await response.json();
}
// Jangan retry untuk error client (4xx)
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error: ${response.status}`);
}
// Server error (5xx) -- bisa di-retry
lastError = new Error(`Server error: ${response.status}`);
} catch (error) {
lastError = error;
// Jangan retry kalau ini client error
if (error.message.startsWith('Client error')) {
throw error;
}
}
if (attempt < maxRetries) {
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, attempt - 1) * 1000;
console.log(`Retry ${attempt}/${maxRetries} dalam ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
Fungsi ini hanya retry untuk server error (5xx), bukan client error (4xx). Karena 404 atau 401 tidak akan berubah kalau di-retry.
Menjalankan async di loop butuh perhatian khusus. Ini tiga pola yang umum:
// 1. Sequential -- satu per satu (lambat tapi terurut)
async function sequentialFetch(urls) {
const results = [];
for (const url of urls) {
const res = await fetch(url);
const data = await res.json();
results.push(data);
}
return results;
}
// 2. Parallel -- semua sekaligus (cepat tapi urutan tidak terjamin)
async function parallelFetch(urls) {
const promises = urls.map(async url => {
const res = await fetch(url);
return res.json();
});
return Promise.all(promises);
}
// 3. Batched -- kelompokkan per batch (seimbang)
async function batchedFetch(urls, batchSize = 3) {
const results = [];
for (let i = 0; i < urls.length; i += batchSize) {
const batch = urls.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(url => fetch(url).then(r => r.json()))
);
results.push(...batchResults);
}
return results;
}
Pakai sequential kalau urutan penting. Pakai parallel kalau semua request independen. Pakai batched kalau mau batasi jumlah request paralel (supaya tidak overload server).
fetch() bawaan JavaScript tidak punya opsi timeout. Kamu harus bikin sendiri pakai AbortController:
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timeout setelah ${timeoutMs}ms`);
}
throw error;
}
}
Ini penting untuk production. Tanpa timeout, request yang hang bisa menghabiskan resource browser.
Debugging kode asynchronous memang tricky. Beberapa tips:
await atau .catch(). Promise yang tidak ditangani bisa menyebabkan error silent
async function benchmark() {
console.time('fetch-users');
const users = await fetch('/api/users').then(r => r.json());
console.timeEnd('fetch-users'); // fetch-users: 234.56ms
console.time('fetch-posts');
const posts = await fetch('/api/posts').then(r => r.json());
console.timeEnd('fetch-posts'); // fetch-posts: 189.23ms
}
Async/await bikin kode asynchronous di JavaScript jadi jauh lebih readable dan maintainable. Kuncinya: selalu gunakan try/catch untuk error handling, manfaatkan Promise.all() untuk request paralel, dan tambahkan timeout untuk production code. Kalau kamu mau belajar lebih lanjut tentang REST API development, cek tutorial Bangun REST API dengan CodeIgniter 4 yang sudah saya tulis sebelumnya. Untuk deployment aplikasi yang pakai fetch API, Docker Compose Multi-Service bisa jadi solusi yang praktis.
Pola async/await yang saya bahas di atas juga berlaku di runtime lain seperti Node.js dan Deno. Jadi kalau kamu bikin backend pakai Node.js, konsep yang sama tetap relevan. Selamat coding!