Aplikasi nyata tidak selalu berjalan mulus: handler bisa melempar error, promise bisa ditolak, dan body JSON kiriman klien bisa rusak. Tanpa strategi yang jelas, setiap handler berakhir dengan try/catch sendiri dan format respons error yang berbeda-beda. Express menyediakan satu mekanisme untuk menampung semuanya: error middleware yang ditulis sekali namun menangkap error dari rute mana pun. Chapter ini membahas cara kerjanya, termasuk perilaku baru Express 5 yang otomatis meneruskan error dari handler async.

Memisahkan 404 dan 500 #

Dua angka ini sering dicampur, padahal sumbernya berbeda. Status 404 berarti klien meminta path yang tidak ada — kesalahan ada di sisi pengirim permintaan. Status 500 berarti rute ditemukan tetapi handler gagal menjalankan tugasnya — kesalahan ada di sisi aplikasi kita. Keduanya perlu bentuk respons yang sama rapihnya, sehingga klien selalu bisa membaca field yang seragam.

Express mengenali dua jalur tersebut secara terpisah. Middleware biasa yang terdaftar setelah semua rute menjadi penampung 404, sedangkan middleware dengan empat argumen menjadi penampung error. Berkas berikut menunjukkan keduanya bekerja pada lima skenario berbeda.

js
import express from 'express';

const host = '127.0.0.1';
const app = express();
app.use(express.json());

// Rute yang melempar error secara sinkron.
app.get('/pecah', (req, res) => {
  throw new Error('Data sumber tidak siap');
});

// Rute async yang promise-nya ditolak.
app.get('/lambat', async (req, res) => {
  await Promise.reject(new Error('Basis data tidak merespons'));
});

// Rute dengan status error khusus.
app.get('/terlarang', (req, res) => {
  const err = new Error('Akses ditolak');
  err.status = 403;
  throw err;
});

// Rute sehat sebagai pembanding.
app.post('/catatan', (req, res) => {
  res.status(201).json({ diterima: req.body });
});

// Middleware 404: terdaftar setelah semua rute.
app.use((req, res) => {
  res.status(404).json({
    error: { status: 404, pesan: `Rute ${req.method} ${req.path} tidak ditemukan` },
  });
});

// Error middleware: wajib punya empat argumen.
app.use((err, req, res, next) => {
  const status = err.status ?? err.statusCode ?? 500;
  res.status(status).json({ error: { status, pesan: err.message } });
});

const server = app.listen(0, host, async () => {
  const base = `http://${host}:${server.address().port}`;
  const r1 = await fetch(`${base}/pecah`);
  console.log('pecah    :', r1.status, await r1.text());
  const r2 = await fetch(`${base}/lambat`);
  console.log('lambat   :', r2.status, await r2.text());
  const r3 = await fetch(`${base}/terlarang`);
  console.log('terlarang:', r3.status, await r3.text());
  const r4 = await fetch(`${base}/catatan`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: '{tidak valid',
  });
  console.log('json rusak:', r4.status, await r4.text());
  const r5 = await fetch(`${base}/tak-ada`);
  console.log('tak ada  :', r5.status, await r5.text());
  server.close();
  server.closeAllConnections();
});

Output yang Dihasilkan:

text
pecah    : 500 {"error":{"status":500,"pesan":"Data sumber tidak siap"}}
lambat   : 500 {"error":{"status":500,"pesan":"Basis data tidak merespons"}}
terlarang: 403 {"error":{"status":403,"pesan":"Akses ditolak"}}
json rusak: 400 {"error":{"status":400,"pesan":"Expected property name or '}' in JSON at position 1 (line 1 column 2)"}}
tak ada  : 404 {"error":{"status":404,"pesan":"Rute GET /tak-ada tidak ditemukan"}}

Mari kita bedah baris-baris yang menentukan:

  1. Baris 8-10 (throw new Error): error sinkron dari handler langsung dilompatkan ke error middleware — perhatikan bahwa handler ini tidak menangkap errornya sendiri.
  2. Baris 13-15 (await Promise.reject): pada Express 5, promise yang ditolak di handler async diteruskan ke error middleware secara otomatis. Keluaran lambat membuktikannya: status 500 datang dari error middleware, bukan dari proses yang berhenti.
  3. Baris 18-22 (err.status = 403): handler dapat menandai error dengan status yang lebih spesifik. Error middleware membaca err.status terlebih dulu, jatuh ke err.statusCode, lalu ke 500 sebagai bawaan.
  4. Baris 30-34 (middleware 404): didaftarkan setelah semua rute agar hanya menerima permintaan yang tidak cocok dengan rute mana pun. Posisi ini penting — jika dipasang sebelum rute, seluruh aplikasi akan berubah menjadi 404.
  5. Baris 37-40 (error middleware): keempat argumennya — err, req, res, next — adalah penanda yang dipakai Express untuk membedakannya dari middleware biasa.

Error dari Handler Async #

Handler async adalah tempat error paling sering muncul, karena setiap await ke basis data atau layanan eksternal bisa gagal. Express 5 menangani hal ini dengan elegan: promise yang ditolak diteruskan ke error middleware seolah-olah handler melempar throw biasa.

Pada Express 4, perilaku tersebut belum ada — promise yang ditolak justru menggantungkan permintaan karena Express tidak menunggunya. Pola yang lazim dipakai saat itu adalah membungkus handler async agar error diteruskan ke next:

js Pola wrapper untuk Express 4 (ilustrasi)
const bungkusAsync = (handler) => (req, res, next) => {
  Promise.resolve(handler(req, res, next)).catch(next);
};

app.get('/lama', bungkusAsync(async (req, res) => {
  const data = await bacaDariBasisData();
  res.json(data);
}));

Bentuk Error yang Konsisten #

Error middleware terbaik adalah yang mengubah semua error menjadi satu bentuk kontrak, misalnya { error: { status, pesan } } seperti contoh kita. Klien cukup memeriksa satu struktur untuk semua kegagalan — baik 400 dari body rusak, 403 dari akses ditolak, maupun 500 dari handler yang gagal. Kontrak ini juga memudahkan pengujian: test cukup membandingkan bentuk respons, bukan memilik pesan satu per satu.

Dengan routing, middleware, dan error handling, kita telah menuntaskan dasar-dasar Express. Ekosistem Node.js juga menumbuhkan framework dengan pendekatan berbeda — lebih cepat, berbasis skema, dengan validasi bawaan. Kita akan mengenal yang paling menonjol pada Chapter Fastify: Dasar.

Referensi #