Postmortem: try/catch middleware yang tidak menangkap apa-apa di Hono
Aku pasang error middleware di Hono v4 dan merasa aman. Ternyata route error melewatinya sepenuhnya. Penyebabnya ada di cara Hono compose dispatch.
What happened
Di project AI WhatsApp bot (Hono v4), error dari route handler kadang balik sebagai response JSON rapi, kadang balik sebagai 500 plain text mentah. Format tidak konsisten, padahal aku sudah pasang satu middleware error handler global di app.use('*', ...).
Timeline
Timeline ini rekonstruksi dari memory — menitnya approximate, urutannya akurat.
T+0— MiddlewaretryCatchterpasang diapp.use('*'). Semua route di bawahnya. Merasa sudah cover.T+10 menit— Test manual: lempar error dari route. Response masih 500 plain text, bukan JSON envelope.T+20 menit— Cek apakah route salah daftar di luar middleware. Bukan. Semua route lewatapp.use('*').T+30 menit— Baca source Honocompose.js. Baru paham root cause.T+35 menit— Migrasi semua handling keapp.onError(). Response konsisten.
Root cause
Dua hal yang aku salah pahami:
1. Error di route handler tidak melewati middleware chain sisa.
Di Hono v4, ketika route handler melempar error, error itu di-catch di level dispatch oleh compose.js — bukan diteruskan ke middleware berikutnya. Jadi middleware try/catch yang “membungkus” route memang tidak akan pernah melihat error itu.
2. Tempat yang benar adalah app.onError().
Hono punya hook khusus untuk ini. Semua error yang lolos dari route handler diarahkan ke sana.
Kode sebelum (yang salah):
// SALAH: tidak menangkap route error
app.use('*', async (c, next) => {
try {
await next();
} catch (err) {
return c.json({ error: 'internal_error' }, 500);
}
});
Perlu dicatat: middleware ini tidak 100% useless — dia masih menangkap error yang dilempar oleh middleware lain di bawahnya. Tapi error dari route handler? Tidak.
Why it slipped through
- Semua happy path test lolos. Error handling tidak masuk checklist test manual.
- Dokumentasi Hono sebenarnya menjelaskan
onError, tapi aku bawa asumsi dari framework lain (Express-style error middleware) tanpa verifikasi. - Tidak ada test yang deliberately melempar error dari route handler.
Fix
const app = new Hono();
app.onError((err, c) => {
console.error('[unhandled]', err);
return c.json(
{
error: 'internal_error',
message: err.message,
},
500,
);
});
Satu hook, satu format error, berlaku untuk semua route. Middleware try/catch yang lama dihapus — dead code yang menyesatkan.
Trade-offs
onErroradalah satu titik global. Kalau butuh format error berbeda per route group, harus branching di dalamonErrorberdasarkan path. Lebih ribet sedikit, tapi eksplisit.- Error yang dilempar middleware tetap bisa ditangani lokal di middleware itu sendiri kalau memang perlu handling khusus.
onErrorjadi fallback terakhir.
Follow-ups
- Tambah test yang melempar error dari route handler, assert format response JSON
- Dokumentasikan pattern ini di README internal project
Lessons
- Asumsi dari framework lain tidak portabel. “Error middleware” di Hono beda semantics dengan Express.
- Kalau error handler tidak pernah di-test dengan error sungguhan, anggap dia tidak bekerja.
- Baca source framework 30 menit lebih murah daripada debugging berdasarkan asumsi.
References
- Hono docs — Error Handling —
app.onErrordanapp.notFound - Case study: AI WhatsApp Bot — project tempat bug ini terjadi
- pgvector untuk RAG — keputusan arsitektur lain di project yang sama