Error handling adalah aturan tentang apa yang terjadi kalau sesuatu gagal: catatan yang dicari gak ada, file data hilang, input aneh. Bayangin restoran. Kalau masakan gagal di dapur, koki gak lari ke meja tamu sambil teriak "kompor nomor 2 bocor!". Dia oper masalahnya ke kepala pelayan, yang menyampaikannya ke tamu dengan sopan dan selalu dengan cara yang sama: "Maaf, menu itu habis" atau "Maaf, dapur sedang bermasalah". Detail kompornya cukup dicatat di buku dapur. Di Express, kepala pelayan itu namanya error middleware.
🧪 Tebakan cepat: handler
asyncdicatatan-apimelempar error untukGET /catatan/9. Di Express 5, client menerima 404 berbentuk JSON. Kode yang persis sama dijalankan dengan Express 4. Apa yang dialami client? Tebak dulu, jawabannya ada di bagian "Express 4 dan Express 5: error dari handler async".
Dari try/catch ke satu tempat untuk semua error
try { ... } catch (err) { ... } menangkap error yang dilempar (throw new Error("...")) di dalam blok try, jadi program gak berhenti di tengah jalan, dan pesannya ada di err.message. Di pelajaran sebelumnya, tiap route menjawab 404-nya sendiri dengan return res.status(404).json({ error: "Catatan tidak ditemukan" }). Makin banyak route, makin banyak gaya menjawab error. Express punya cara yang lebih rapi: route cukup melempar error, lalu satu error middleware di paling bawah yang menjawab semuanya.
Error middleware: empat parameter, di paling bawah
app.js milik catatan-api, dengan data dari data/catatan.json seperti di pelajaran modul bawaan:
// Express 5: npm install express
const express = require("express");
const fs = require("node:fs/promises");
const path = require("node:path");
const app = express();
function httpError(status, message) {
const err = new Error(message);
err.status = status;
return err;
}
async function cariCatatan(id) {
const isi = await fs.readFile(path.join(__dirname, "data", "catatan.json"), "utf8");
return JSON.parse(isi).find((c) => c.id === id);
}
app.get("/catatan/:id", async (req, res) => {
const id = Number(req.params.id);
if (!Number.isInteger(id)) throw httpError(400, "id harus angka");
const catatan = await cariCatatan(id);
if (!catatan) throw httpError(404, "Catatan tidak ditemukan");
res.json(catatan);
});
// Error middleware: setelah semua route, dengan empat parameter
app.use((err, req, res, next) => {
const status = err.status || 500;
if (status === 500) console.error(err);
const pesan = status === 500 ? "Terjadi kesalahan di server" : err.message;
res.status(status).json({ error: pesan });
});
app.listen(3000, () => {
console.log("Server jalan di http://localhost:3000");
});
httpError(status, message)membuatErrorbiasa lalu menempelkanstatus, supaya error middleware tahu status mana yang dipakai.throw httpError(404, ...): Express menangkap error-nya, melompati middleware biasa, dan langsung memanggil error middleware.app.use((err, req, res, next) => {...})dikenali sebagai error middleware dari jumlah parameternya: empat, denganerrdi depan.err.status || 500: error tanpa status (misalnya file hilang) dianggap salah server.- Bentuk jawabannya selalu
{ "error": "..." }, sama dengan 404 di pelajaran sebelumnya, jadi client gak perlu berubah. - Untuk 500, client cuma dapat kalimat umum. Detailnya dicetak
console.error(err)ke terminal server.
Empat request, empat jawaban
| Request | Yang terjadi | Status | Body |
|---|---|---|---|
GET /catatan/1 |
catatan ketemu | 200 | {"id":1,"judul":"Belanja","isi":"beli beras 5 kg"} |
GET /catatan/9 |
gak ketemu | 404 | {"error":"Catatan tidak ditemukan"} |
GET /catatan/abc |
Number("abc") hasilnya NaN |
400 | {"error":"id harus angka"} |
GET /catatan/1, file data hilang |
readFile gagal: ENOENT |
500 | {"error":"Terjadi kesalahan di server"} |
400 artinya permintaannya salah, 404 yang diminta gak ada, 500 (Internal Server Error) salahnya di server. Untuk baris terakhir, terminal server mencetak error lengkapnya, termasuk baris-baris at .... Itu namanya stack trace: jejak function mana memanggil function mana sampai error terjadi, dengan nama file dan nomor barisnya.
Express 4 dan Express 5: error dari handler async
Function async selalu mengembalikan Promise, dan throw di dalamnya membuat Promise itu ditolak (rejected). Express 5 menunggu Promise dari handler; kalau ditolak, ia memanggil next(err) untukmu, jadi error-nya sampai ke error middleware.
Express 4 gak melihat Promise itu sama sekali. Penolakannya gak ditangkap siapa pun: unhandled promise rejection, dan sejak Node 15 itu mematikan proses:
/home/budi/catatan-api/app.js:9
const err = new Error(message);
^
Error: Catatan tidak ditemukan
at httpError (/home/budi/catatan-api/app.js:9:15)
at /home/budi/catatan-api/app.js:23:23 {
status: 404
}
Node.js v24.18.0
Client yang bertanya dapat curl: (52) Empty reply from server, dan semua request sesudahnya curl: (7) Failed to connect to localhost port 3000. Itu jawaban tebakannya: satu catatan yang gak ada mematikan server untuk semua orang.
Di Express 4, handler async harus meneruskan error-nya sendiri:
app.get("/catatan/:id", async (req, res, next) => {
try {
const catatan = await cariCatatan(Number(req.params.id));
if (!catatan) throw httpError(404, "Catatan tidak ditemukan");
res.json(catatan);
} catch (err) {
next(err); // wajib di Express 4
}
});
Supaya gak menulis try/catch di setiap route, banyak proyek Express 4 memakai pembungkus asyncHandler. Gunanya mengurangi boilerplate try/catch: penolakan apa pun otomatis diteruskan ke next.
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get("/catatan/:id", asyncHandler(async (req, res) => { /* isi yang sama */ }));
Stack trace jangan sampai ke client
Error middleware yang menulis res.status(status).json({ error: err.message, stack: err.stack }) kelihatan membantu. Tapi saat file data hilang, siapa pun yang membuka GET /catatan/1 menerima "error":"ENOENT: no such file or directory, open '/home/budi/catatan-api/data/catatan.json'" plus stack trace dengan nomor baris kodemu. Orang asing sekarang tahu nama user server, folder proyek, dan nama file datanya: peta gratis untuk penyerang.
Error handler bawaan Express (yang menjawab kalau kamu gak punya error middleware) mengirim stack trace dalam HTML selama NODE_ENV bukan production; dengan NODE_ENV=production isinya cuma Internal Server Error. Error middleware buatanmu gak melihat NODE_ENV, jadi dia sendiri yang memilih: detail ke log server, kalimat umum ke client.
Di latihan: getUser dengan try/catch
Latihannya fungsi biasa tanpa server, tapi polanya sama. Kode awal punya array users (Budi dan Siti) dan getUser(id) dengan try/catch. Di catch, cetak "getUser(" + id + "): Error: " + err.message lalu return null. User gak ketemu: cetak barisnya lalu return null. User ketemu: cetak datanya lalu return user.
getUser(1): {"id":1,"name":"Budi"}
getUser(99): Error 404: User tidak ditemukan
getUser(abc): Error: Invalid ID
⚠️ Jebakan yang sering ketemu
- Error middleware dengan tiga parameter.
(err, req, res) => {...}dianggap middleware biasa dan dilewati, jadi yang menjawab handler bawaan Express dengan halaman HTML. Tulis keempatnya, termasuknextwalau gak dipakai. - Error middleware ditaruh sebelum route. Error dari route di bawahnya gak pernah sampai. Taruh paling bawah, sebelum
app.listen. - Handler async di Express 4 tanpa
try/catch. Satu error mematikan server. Pakaitry/catchplusnext(err),asyncHandler, atau naik ke Express 5 (npm install expresssekarang memasang Express 5; cek dengannpm ls express). - Melempar error dari dalam callback
setTimeout. Express mana pun gak bisa menangkapnya, dan proses mati. Pakai Promise danawait. - Mengirim
err.stackatau pesan asli error 500 ke client. Detail untuk log, kalimat umum untuk client.
TL;DR: Route melempar error (throw) atau memanggil next(err), lalu satu error middleware dengan empat parameter (err, req, res, next), ditaruh setelah semua route, menjawab semuanya dengan bentuk yang sama, { "error": "..." }. Status yang tepat: 400 permintaan salah, 404 gak ada, 500 salah server. Express 5 meneruskan error dari handler async otomatis; Express 4 gak, jadi error-nya jadi unhandled promise rejection yang mematikan server, kecuali kamu memakai try { ... } catch (err) { next(err) } atau asyncHandler. Stack trace cuma untuk log server, gak pernah untuk client. Di catch, cetak err.message, bukan kalimat tetap.