![]() |
| gambar ilustrasi di atas: perbandingan pola try/catch/finally dasar dengan pendekatan custom error class untuk penanganan error yang lebih spesifik |
Salah satu kesalahan paling umum yang dilakukan developer JavaScript adalah menulis kode async/await tanpa penanganan error yang memadai — lalu terkejut ketika aplikasi tiba-tiba crash di production tanpa pesan yang jelas. Padahal, async/await sebenarnya membuat error handling jauh lebih rapi dibanding .then().catch() berantai, asalkan dipakai dengan teknik yang tepat.
Kesalahan Paling Dasar: Lupa try/catch Sama Sekali
Ini terdengar sepele, tapi sangat sering terjadi:
// ❌ Tidak ada penanganan error sama sekali
async function getUser(id) {
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
return data;
}
Jika fetch gagal — entah karena jaringan putus atau server down — error ini akan menjadi unhandled promise rejection yang bisa menghentikan alur program secara tidak terduga, atau bahkan membuat seluruh proses Node.js crash tanpa peringatan yang jelas.
Pola Dasar yang Wajib Dikuasai: try/catch/finally
async function getUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
throw new Error(`HTTP error: ${res.status}`);
}
return await res.json();
} catch (err) {
console.error("Gagal mengambil data user:", err);
return null;
} finally {
setLoading(false);
}
}
Perhatikan detail penting di sini: fetch() tidak otomatis melempar error hanya karena respons berstatus 404 atau 500 — ia hanya reject jika terjadi kegagalan jaringan. Karena itu, pengecekan res.ok secara manual jadi wajib untuk menangkap error HTTP.
Jangan Perlakukan Semua Error dengan Cara yang Sama
Salah satu peningkatan paling berdampak adalah membedakan jenis error, bukan menangani semuanya secara generik. Di sinilah custom error class jadi berguna:
class NetworkError extends Error {
constructor(message) {
super(message);
this.name = "NetworkError";
}
}
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
async function submitForm(data) {
try {
validateData(data); // bisa throw ValidationError
await sendToServer(data); // bisa throw NetworkError
} catch (err) {
if (err instanceof ValidationError) {
showFieldError(err.field, err.message);
} else if (err instanceof NetworkError) {
showRetryPrompt();
} else {
logToSentry(err); // error tak terduga, catat untuk investigasi
}
}
}
Dengan pendekatan ini, aplikasi bisa merespons setiap jenis kegagalan dengan cara yang paling sesuai — alih-alih menampilkan pesan error generik untuk semua kasus.
Menangani Banyak Operasi Async Sekaligus
Ketika bekerja dengan beberapa await berurutan, penting memikirkan di mana sebenarnya error harus ditangkap:
// ❌ Satu try/catch besar menyembunyikan sumber error
try {
const user = await fetchUser(id);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
} catch (err) {
console.error("Ada yang gagal, entah di mana:", err);
}
// ✅ Menangkap error dengan konteks yang jelas
async function loadUserData(id) {
const user = await fetchUser(id).catch(err => {
throw new Error(`Gagal memuat user: ${err.message}`);
});
const posts = await fetchPosts(user.id).catch(err => {
throw new Error(`Gagal memuat posts: ${err.message}`);
});
return { user, posts };
}
Menambahkan konteks di setiap titik kegagalan membuat proses debugging jauh lebih cepat — dibanding hanya mendapat pesan error generik tanpa tahu operasi mana yang sebenarnya bermasalah.
Retry Otomatis untuk Error yang Bersifat Sementara
Tidak semua error perlu langsung menyerah. Untuk kegagalan jaringan yang sifatnya sementara, pola retry dengan exponential backoff sering jadi solusi yang efektif:
async function fetchWithRetry(url, retries = 3, delay = 1000) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`Status: ${res.status}`);
return await res.json();
} catch (err) {
if (retries === 0) throw err;
await new Promise(r => setTimeout(r, delay));
return fetchWithRetry(url, retries - 1, delay * 2);
}
}
Jangan Lupakan finally untuk Cleanup
Blok finally sering diabaikan, padahal sangat berguna untuk memastikan sesuatu tetap dijalankan — baik operasi berhasil maupun gagal. Ini paling umum dipakai untuk mematikan status loading:
async function loadData() {
setLoading(true);
try {
const data = await fetchData();
setData(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false); // selalu dijalankan, apapun hasilnya
}
}
Tanpa finally, developer sering lupa mematikan status loading di jalur error — menyebabkan UI terjebak dalam kondisi loading selamanya ketika terjadi kegagalan.
Checklist Error Handling yang Solid
- Selalu bungkus
awaitdengan try/catch, terutama untuk operasi yang berinteraksi dengan jaringan atau sistem eksternal - Periksa
res.oksetelahfetch(), karena error HTTP tidak otomatis melempar exception - Gunakan custom error class untuk membedakan jenis kegagalan dan menentukan respons yang sesuai
- Tambahkan konteks di setiap titik kegagalan, bukan satu try/catch besar yang menyamarkan sumber masalah
- Pertimbangkan retry logic untuk error yang bersifat sementara
- Manfaatkan
finallyuntuk memastikan proses cleanup selalu berjalan
Error handling yang baik bukan soal menangkap semua exception secara membabi buta, melainkan soal memahami jenis kegagalan apa yang mungkin terjadi dan merespons masing-masing dengan cara yang tepat. Investasi waktu sedikit lebih banyak di awal untuk menyusun strategi error handling yang solid akan jauh lebih murah dibanding harus melakukan debugging di tengah malam saat aplikasi production tiba-tiba bermasalah.
💬 Komentar ()
Punya pertanyaan atau tanggapan? Yuk diskusi di kolom komentar.
✍ Tulis Komentar