Request yang gagal tetap harus dijawab dengan rapi. Error handling di API artinya tiga hal: status code yang tepat, bentuk body error yang sama di semua endpoint, dan gak ada detail dalaman server yang ikut terkirim. Bayangin papan jadwal di bandara. Penerbangan yang bermasalah selalu ditulis dengan format yang sama: kode penerbangan, status (DELAYED, CANCELLED), dan satu kalimat untuk penumpang. Penumpang gak perlu tahu baut mana yang rusak. Detail kerusakannya dicatat teknisi di buku log, bukan ditempel di papan.
🧪 Tebakan cepat: handler
GET /books/1/reviewspunya bug, dan error handler-nya menjawabres.status(500).json({ error: err.stack }). Apa yang diterima client: pesan "Something went wrong", stack trace lengkap dengan path file server, atau body kosong? Tebak dulu, jawabannya ada di bagian "Jangan kirim stack trace".
Status code: 4xx salah client, 5xx salah server
Digit pertama status code sudah memberi tahu siapa yang perlu bertindak:
- 4xx (400 sampai 499): error di sisi client. Request-nya yang perlu diperbaiki. Mengirim ulang request yang sama hasilnya tetap sama.
- 5xx (500 sampai 599): error di sisi server. Client gak salah. Mencoba lagi nanti mungkin berhasil.
| Status | Kapan | Contoh di Toko Buku |
|---|---|---|
400 Bad Request |
request-nya rusak, gak bisa dibaca | body JSON yang kurung kurawalnya gak ditutup |
401 Unauthorized |
belum dikenali | GET /profile tanpa token |
403 Forbidden |
sudah login, tapi gak punya izin | Siti (customer) menghapus buku |
404 Not Found |
yang diminta gak ada | GET /books/99 |
409 Conflict |
bentrok dengan data yang sudah ada | menambah buku yang judulnya sudah terdaftar |
422 Unprocessable Content |
terbaca, tapi melanggar aturan | harga -5000 |
500 Internal Server Error |
bug di server | book.reviews ternyata undefined |
503 Service Unavailable |
server sementara gak bisa melayani | database sedang mati |
User yang sudah login tapi gak berhak atas sebuah resource dijawab 403, bukan 401.
Satu bentuk error untuk semua endpoint
Kalau tiap route menulis error-nya sendiri ({"error": "..."} di sini, {"message": "..."} di sana, {"err": true} di tempat lain), frontend harus menebak bentuknya satu per satu. Pilih satu bentuk, pakai di mana-mana. Dua bentuk yang umum, untuk kesalahan yang sama:
Bentuk sederhana, yang dipakai latihan:
{ "error": { "code": "NOT_FOUND", "message": "Book not found" } }
codeadalah kode tetap untuk dibaca program. Frontend menulisif (body.error.code === "NOT_FOUND"), gak bergantung pada kalimatnya.messageadalah kalimat untuk manusia, boleh berubah kapan saja.
Problem Details (RFC 9457), standar resmi untuk error di HTTP API, dengan header Content-Type: application/problem+json:
{
"type": "https://api.tokobuku.test/problems/not-found",
"title": "Not Found",
"status": 404,
"detail": "Book 99 does not exist.",
"instance": "/books/99"
}
type: alamat (URI) yang menamai jenis error-nya, sering sekaligus halaman dokumentasinya. Kalau gak ada keterangan lebih dari status-nya, isinyaabout:blank.title: ringkasan singkat jenis error-nya, sama untuk semua kejadian dengantypeyang sama.status: angka yang sama dengan status HTTP-nya.detail: penjelasan untuk kejadian yang ini.instance: kejadian yang mana, misalnya path request-nya.- Field tambahan boleh, misalnya
errorsper field dari pelajaran Validasi Input.
Dua-duanya baik. Yang penting: satu bentuk untuk semua endpoint.
Error handler terpusat
Di Express, route cukup melempar error-nya. Satu middleware khusus di paling akhir yang menjawab semuanya:
class HttpError extends Error {
constructor(status, code, message) {
super(message);
this.status = status;
this.code = code;
}
}
app.get("/books/:id", (req, res) => {
const book = books.find((b) => b.id === Number(req.params.id));
if (!book) throw new HttpError(404, "NOT_FOUND", "Book not found");
res.json({ data: book });
});
// Paling akhir, sesudah semua route:
app.use((err, req, res, next) => {
const status = err.status || 500;
const code = err.code || "INTERNAL_ERROR";
const message = status === 500 ? "Something went wrong" : err.message;
if (status === 500) console.error(err); // detail lengkap cuma masuk log server
res.status(status).json({ error: { code, message } });
});
class HttpError extends Errormembuat error yang membawastatusdancode.throwmenghentikan route dan melompat ke error handler.next(err)melakukan hal yang sama, dan itu yang dipakai di dalam callback.- Express mengenali error handler dari empat parameternya:
(err, req, res, next). Dengan tiga parameter, dia dianggap middleware biasa dan dilewati saat ada error. app.use(...)untuk error handler ditaruh sesudah semua route.err.status || 500: error tanpastatusartinya bug yang gak terduga, jadi500, dengan pesan umum.- Handler
async: Express 5 meneruskan error dariawaitke error handler sendiri. Di Express 4, bungkus dengantry/catchlalu panggilnext(err). Lanjutannya ada di pelajaran Error Handling di track Node.js.
| Request | Yang terjadi | Response |
|---|---|---|
GET /books/1 |
bukunya ada | 200 {"data": {...}} |
GET /books/99 |
throw new HttpError(404, ...) |
404 {"error": {"code": "NOT_FOUND", "message": "Book not found"}} |
GET /books/1/reviews |
TypeError, bug |
500 {"error": {"code": "INTERNAL_ERROR", "message": "Something went wrong"}} |
Jangan kirim stack trace
Stack trace adalah catatan di mana error terjadi: nama error, file, dan nomor baris. Dengan res.status(500).json({ error: err.stack }), client menerima kira-kira ini:
TypeError: Cannot read properties of undefined (reading 'map')
at /home/deploy/tokobuku-api/src/routes/reviews.js:14:31
at Layer.handle [as handle_request] (/home/deploy/tokobuku-api/node_modules/express/lib/router/layer.js:95:5)
Itu jawaban tebakannya: stack trace lengkap ikut terkirim. Path folder server, nama file, nomor baris, dan library yang dipakai jadi peta untuk penyerang, sementara pengguna gak bisa berbuat apa-apa dengannya. Client cukup dapat INTERNAL_ERROR dan "Something went wrong". Detailnya masuk log server lewat console.error(err), tempat developer mencarinya.
⚠️ Jebakan yang sering ketemu
- Error handler dengan tiga parameter.
(err, req, res)tanpanextdianggap middleware biasa. Tulis keempatnya, walaunextgak dipakai. - Error handler di atas route. Middleware jalan berurutan. Pasang
app.use((err, req, res, next) => ...)paling akhir. - Error dijawab 200.
res.json({ error: "..." })berstatus200, danfetchdi frontend menganggapres.okbenar. Pakai status 4xx atau 5xx. - Kesalahan client dijawab 500. Validasi gagal itu
400atau422.500bikin client mengira server rusak dan mencoba lagi percuma. - Stack trace atau pesan SQL ke client. Kirim pesan umum, simpan detailnya di log.
TL;DR: 4xx berarti error di sisi client (perbaiki request-nya), 5xx berarti error di sisi server. 400 request rusak, 401 belum dikenali, 403 sudah login tapi gak punya izin, 404 gak ada, 409 bentrok, 422 melanggar aturan, 500 bug. Pakai satu bentuk error di semua endpoint: {"error": {"code": "NOT_FOUND", "message": "..."}}, atau Problem Details RFC 9457 (type, title, status, detail, instance, dengan Content-Type: application/problem+json). Route melempar error (throw atau next(err)), dan satu error handler dengan empat parameter (err, req, res, next), dipasang paling akhir lewat app.use, membaca err.status dan menjawab semuanya. Error tak terduga jadi 500 INTERNAL_ERROR "Something went wrong"; stack trace cuma masuk log, gak pernah ke client.