Javascript Error Handling Try Catch Throw Custom Error Debugging
Selesai membaca dokumentasi ini?
Kembali ke rujukan utama JavaScript untuk melanjutkan topik lainnya.
Kembali ke rujukan utama JavaScript untuk melanjutkan topik lainnya.
Error handling adalah cara kita membuat program tetap bisa bertahan saat sesuatu gagal. Di JavaScript, kegagalan bisa datang dari banyak arah: parsing data, akses properti yang tidak ada, validasi input, request network, operasi async, sampai bug logika yang baru kelihatan di runtime.
Tujuan error handling bukan untuk menyembunyikan masalah. Tujuannya justru untuk:
Kalau ditulis dengan baik, error handling membuat flow aplikasi lebih mudah dibaca. Kalau ditulis asal-asalan, error justru hilang, tertelan, atau muncul jauh dari sumber masalah.
Banyak bug JavaScript bukan bug yang langsung membuat aplikasi crash. Yang lebih sering terjadi adalah:
catch dipakai terlalu luas sehingga sumber error jadi kaburDi titik ini, error handling yang rapi membantu dua hal sekaligus:
JavaScript punya dua bentuk kegagalan yang umum:
Kode synchronous bisa melempar error langsung dengan throw.
function parseAge(value) {
const age = Number(value);
if (Number.isNaN(age)) {
throw new Error('Age harus berupa angka');
}
return age;
}Kalau throw terjadi, eksekusi fungsi berhenti saat itu juga dan kontrol pindah ke catch terdekat yang bisa menangkapnya.
Operasi async biasanya gagal lewat Promise rejection, bukan throw biasa.
async function loadUser() {
const response = await fetch('/api/user');
if (!response.ok) {
throw new Error('Gagal memuat user');
}
return response.json();
}Di dalam async function, throw akan berubah menjadi Promise rejection. Ini penting karena error async hanya tertangkap kalau kita memang menangkap Promise-nya dengan benar.
Tangani error di level function kalau fungsi itu memang punya tanggung jawab untuk:
function getDiscountPrice(price, discount) {
if (typeof price !== 'number') {
throw new TypeError('price harus number');
}
if (typeof discount !== 'number') {
throw
Di sini, fungsi berhak menolak input yang salah. Itu lebih baik daripada membiarkan hasil salah muncul diam-diam.
function normalizeUser(user) {
if (!user || typeof user !== 'object') {
throw new TypeError('user wajib object');
}
if (!user.id) {
throw new Error
Fungsi ini tidak perlu tahu UI atau API. Ia hanya menjaga bentuk data yang benar di level domain.
Di level function, kita biasanya ingin:
Kalau fungsi kecil tahu apa yang salah, caller di atasnya jadi lebih mudah menentukan tindak lanjut.
Boundary adalah titik keluar-masuk sistem: handler UI, controller API, route handler, action, atau layer integrasi ke layanan luar.
Tangani error di boundary kalau tujuannya adalah:
Di UI, error biasanya ditangani agar user mendapat umpan balik yang jelas.
async function handleSubmit() {
try {
await saveProfile(formData);
showToast('Profil tersimpan');
} catch (error) {
showToast('Gagal menyimpan profil');
console.error(error);
}
}Boundary UI tidak selalu perlu tahu detail teknisnya. Yang penting:
Di API, error biasanya diterjemahkan menjadi response yang konsisten.
app.post('/users', async (req, res) => {
try {
const user = await createUser(req.body);
res.status(201).json(user);
} catch
Di boundary API, kita sering memilih untuk:
Kalau semua error dibiarkan naik ke boundary, biasanya akibatnya:
Jadi polanya bukan “tangani semuanya di boundary”, melainkan:
try/catch dipakai untuk apatry/catch dipakai untuk mengeksekusi kode yang mungkin gagal dan menangkap error yang dilempar di blok itu.
try {
const data = JSON.parse(input);
console.log(data);
} catch (error) {
console.error('JSON tidak valid');
}try bukan alat untuk menutup semua bugcatch bukan tempat untuk menelan error tanpa jejaktry/catch paling berguna kalau kita ingin memutus alur dengan cara yang terkontroltry {
const parsed = JSON.parse(input);
return parsed;
} catch (error) {
throw new Error(`Input JSON tidak valid: ${error.message}`);
}Di sini, error asli diperkaya konteksnya. Ini membantu debugging tanpa kehilangan alasan kegagalan.
throw dan kapan dipakaithrow dipakai saat program harus berhenti karena kondisi yang tidak bisa dilanjutkan.
Pakai throw ketika:
Jangan pakai throw untuk hal yang sebenarnya hanya status biasa. Misalnya, kondisi “data belum ada” kadang lebih cocok sebagai null, undefined, atau result object, bukan exception.
function requireAuth(user) {
if (!user) {
throw new Error('User belum login');
}
return user;
}function findUser(users, id) {
const user = users.find(item => item.id === id);
if (!user) {
throw new Error('User tidak ditemukan');
}
return
Ini bisa tepat di beberapa domain, tapi kadang lebih enak mengembalikan null jika “tidak ditemukan” adalah hasil normal yang memang diharapkan. Pilih sesuai kontrak fungsi.
Custom error berguna kalau error biasa belum cukup jelas untuk membedakan jenis kegagalan.
class ValidationError extends Error {
constructor(message, details = {}) {
super(message);
this.name = 'ValidationError';
this.details = details;
}
}Custom error membantu kita:
catch lebih spesifikfunction validateEmail(email) {
if (typeof email !== 'string' || !email.includes('@')) {
throw new ValidationError('Email tidak valid', {
field: 'email',
value: email,
});
Di boundary, kita bisa mengenali jenis error ini:
try {
validateEmail(input.email);
} catch (error) {
if (error instanceof ValidationError) {
return res.status(422).json({ message: error.message, details: error.details });
}
return res.status(500).json
Custom error layak dipakai saat:
Kalau proyek masih kecil, custom error tidak wajib. Tapi begitu error handling mulai bercabang, custom error biasanya sangat membantu.
Error yang dilempar di level bawah biasanya naik ke caller terdekat. Kalau tidak ditangkap, error terus naik sampai boundary terluar.
Debugging error handling bukan cuma soal “menangkap error”. Yang lebih penting adalah cara berpikir saat error muncul.
Kalau error muncul di UI, belum tentu sumbernya di UI. Bisa jadi sumbernya:
Pertanyaan pertama yang bagus biasanya:
Error yang terlalu umum seperti Something went wrong memang aman untuk user, tapi tidak cukup untuk debugging.
Gunakan dua lapis pesan:
Kalau error tampak “menghilang”, cek apakah:
catch kosongTidak semua error harus dipulihkan.
Kalau semua error dipaksa “ditangani”, kode sering jadi lebih rapuh.
Bagian ini paling sering memunculkan bug yang susah dilacak.
try/catch di luar await tidak menangkap rejectiontry {
fetch('/api/data').then(res => res.json());
} catch (error) {
console.error('Tidak akan masuk sini untuk rejection async');
}Kenapa? Karena error async terjadi setelah blok try selesai. Kalau pakai Promise, error harus ditangani lewat .catch() atau await di dalam try/catch.
try {
const res = await fetch('/api/data');
const data = await res.json();
} catch (error) {
console.error('Ini menangkap error async');
}return atau await di chain Promisefunction save() {
return apiCall().then(() => {
doNextStep();
});
}Kalau ada Promise di dalam callback yang tidak di-return, error bisa lepas dari rantai yang sedang ditangani.
catch kosongtry {
await riskyOperation();
} catch (error) {
// kosong
}Ini salah satu jebakan paling mahal. Error memang tidak terlihat, tapi bug tetap ada. Minimal log, rethrow, atau ubah menjadi fallback yang jelas.
try {
await loadData();
renderUI();
} catch (error) {
showFallback();
}Kalau renderUI() bug, error itu ikut tertutup dan kelihatan seperti gagal load data. Pisahkan area yang memang rawan gagal.
Semua error async sering diperlakukan sama. Padahal:
fetch gagal jaringanresponse.ok === false berarti server menolakJSON.parse gagal formatKalau semua disamaratakan, debugging jadi lebih lambat.
Kalau callback lama, Promise, dan async/await dipakai bersamaan tanpa pola yang jelas, error path jadi mudah terlewat.
function createPayload(input) {
if (!input.title) {
throw new ValidationError('Title wajib diisi', { field: 'title' });
}
return {
title: input.title.trim(),
async function loadDashboard() {
try {
return await fetchDashboardData();
} catch (error) {
throw new Error(`loadDashboard gagal: ${error.message}`);
}
}async function onSaveClick() {
try {
setSaving(true);
await saveSettings();
notify('Settings tersimpan');
} catch (error) {
notify('Gagal menyimpan settings');
console.error(error);
try/catch untuk semua baris dalam satu file hanya karena merasa aman.cause bila environment mendukung.throw new Error('Gagal memuat profil', { cause: error });throw: menghentikan eksekusi normal dan melempar errortry/catch: menangkap error synchronous, dan async jika error berasal dari awaitPola yang paling aman biasanya sederhana:
await + try/catch atau .catch() yang benarKalau pola ini konsisten, error handling jadi alat kontrol alur, bukan sekadar formalitas.