Membangun error handling yang benar: middleware error empat argumen, async error handling otomatis Express 5, custom error class, dan centralized error handler untuk response error yang konsisten.

Setelah di episode 6 kalian membangun endpoint dengan response lengkap, episode 7 menjawab pertanyaan yang tak terhindarkan: apa yang terjadi saat sesuatu gagal? Database down, input tak terduga, bug — semua aplikasi produksi menghadapinya. Yang membedakan aplikasi profesional adalah cara kegagalan ditangani.
Mengapa error handling layak mendapat episode sendiri? Karena error yang tidak tertangani di Node.js bisa mematikan proses (uncaught exception) atau membuat request menggantung tanpa response. Error handler terpusat memastikan: semua error ditangkap di satu tempat, response error konsisten, dan detail teknis tidak bocor ke client.
Middleware error punya empat parameter — err, req, res, next — dan dipasang di paling akhir pipeline:
app.use((req, res, next) => {
res.status(404).json({ error: "NOT_FOUND", message: "Route tidak ditemukan" })
})
app.use((err, req, res, next) => {
console.error(err)
res.status(500).json({ error: "INTERNAL_ERROR", message: "Terjadi kesalahan" })
})Urutan di atas penting: 404 handler menangkap semua request yang tidak cocok route mana pun, lalu error handler (empat argumen) menangkap semua error. Express membedakannya murni dari jumlah parameter fungsi.
Middleware dan handler mengirim error dengan memanggil next(err):
app.get("/users/:id", (req, res, next) => {
const id = Number(req.params.id)
if (!Number.isInteger(id)) {
const err = new Error("ID tidak valid")
err.status = 400
return next(err)
}
next()
})Begitu next(err) dipanggil, Express melompati seluruh pipeline normal dan langsung menjalankan error handler terdekat. Request yang memanggil next(err) tidak perlu mengirim response sendiri.
Sebelum Express 5, error dari Promise di handler async tidak otomatis diteruskan:
app.get("/users", async (req, res, next) => {
try {
const users = await db.findMany()
res.json(users)
} catch (err) {
next(err)
}
})Melupakan try/catch membuat rejection menjadi unhandled rejection — request menggantung, dan di Node modern bisa menghentikan proses.
Express 5 secara otomatis menangkap rejection dari handler async dan meneruskannya ke error handler:
app.get("/users", async (req, res, next) => {
const users = await db.findMany()
res.json(users)
})Jika db.findMany() melempar error, Express 5 otomatis memanggil error handler tanpa try/catch manual. Ini alasan utama banyak tim bermigrasi ke Express 5 — kode async jadi jauh lebih bersih.
Note
Express 5 menangkap rejection di handler async, tetapi bukan untuk Promise yang kalian lepas tanpa await (fire-and-forget). Untuk operasi latar belakang seperti itu, tangani rejection sendiri — topik ini kembali di episode 22 saat membahas background jobs.
Agar error bisa membawa status dan detail, buat class error sendiri:
export class AppError extends Error {
constructor(status, code, message) {
super(message)
this.status = status
this.code = code
this.name = "AppError"
}
}
export const notFound = (id) =>
new AppError(404, "USER_NOT_FOUND", `User ${id} tidak ditemukan`)Error ini lalu dilempar langsung di handler:
import { AppError } from "./utils/AppError.js"
app.get("/users/:id", async (req, res, next) => {
const user = await db.findById(req.params.id)
if (!user) throw new AppError(404, "USER_NOT_FOUND", "User tidak ditemukan")
res.json(user)
})Dengan Express 5, throw di dalam handler async langsung ditangkap — tidak perlu return next(err).
Error handler terpusat membaca properti custom error dan menyeragamkan bentuk response:
import { AppError } from "../utils/AppError.js"
export const errorHandler = (err, req, res, next) => {
const status = err.status || 500
const code = err.code || "INTERNAL_ERROR"
if (status >= 500) {
console.error(err)
}
res.status(status).json({
error: code,
message: err.message,
...(process.env.NODE_ENV === "development" ? { stack: err.stack } : {}),
})
}Tiga keputusan desain yang penting:
{ error, message }, konsisten dengan episode 6.Handler ini dipasang terakhir di app.js, setelah semua route:
import { errorHandler } from "./middleware/errorHandler.js"
app.use("/api", apiRouter)
app.use((req, res) => res.status(404).json({ error: "NOT_FOUND", message: "Route tidak ditemukan" }))
app.use(errorHandler)next pada Middleware BiasaMiddleware yang error tapi memanggil next() tanpa argumen justru meneruskan request normal — bukan error. Selalu next(err) saat ingin masuk jalur error.
Error handler hanya menangkap error dari middleware/router yang berada di atasnya. Pasang di paling akhir setelah semua route.
Mengirim err.stack atau pesan database mentah ke client membocorkan struktur internal. Batasi ke kode dan pesan yang aman untuk publik.
Tip
Log error 5xx dengan struktur lengkap (episode 17) — stack trace di log server, bukan di response. Client hanya perlu tahu ada kesalahan; tim kalian yang perlu detail.
Episode 7 membangun fondasi ketahanan aplikasi: middleware error empat argumen, next(err) sebagai pintu masuk jalur error, async error handling otomatis Express 5, AppError sebagai bahasa error bersama, dan centralized error handler sebagai satu-satunya gerbang response error.
Inti yang harus dibawa pulang:
next(err) melompati pipeline normal menuju error handler.AppError(status, code, message) membawa konteks error lintas lapisan.Di episode 8 selanjutnya kita akan membawa struktur aplikasi ke level berikutnya: router dan modular app — express.Router(), pemecahan route per resource, dan refactor aplikasi Hello World menjadi modul yang rapi. Sampai jumpa di episode 8!