Error Handling — Node.js

Error handling yang baik membuat API kamu lebih robust dan user-friendly. Try/catch di route handler: app.get("/users/:id", (req, res) => { try { const user =…

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 async di catatan-api melempar error untuk GET /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");
});

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

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.

Yang akan kamu pelajari