Error handling
When no route matches, Hono answers with the text 404 Not Found, and when the code throws, it answers with Internal Server Error and code 500. For an API this is awkward: the client expects JSON. Hono lets you replace both responses and throw errors with the right code straight from a handler.
Unknown addresses
In src/index.ts, after mounting the route groups, add:
// src/index.ts
app.notFound((c) => {
return c.json({ error: `Address ${c.req.path} not found` }, 404)
})Open http://localhost:3000/nothing and you get the JSON {"error":"Address /nothing not found"} with code 404.
HTTPException
To stop processing with a meaningful code, throw an HTTPException:
// src/routes/posts.ts
import { HTTPException } from 'hono/http-exception'
posts.get('/:id', (c) => {
const post = allPosts.find((p) => p.id === Number(c.req.param('id')))
if (!post) {
throw new HTTPException(404, { message: 'Post not found' })
}
return c.json(post)
})Here allPosts is the array of posts from earlier lessons. The request /posts/99 returns code 404 and the text "Post not found". An exception is handy in helper functions and middleware, where you cannot return a response with return.
A shared error handler
app.onError() receives every error a handler did not catch:
// src/index.ts
import { HTTPException } from 'hono/http-exception'
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status)
}
console.error(err)
return c.json({ error: 'Internal server error' }, 500)
})Now /posts/99 returns the JSON {"error":"Post not found"} with code 404. Any other error, such as a typo in the code, is written to the terminal, and the client gets a short message with code 500.
What handles what
| Situation | Who answers |
|---|---|
| no matching route | app.notFound() |
throw new HTTPException(...) | app.onError() |
| any other error in the code | app.onError() |
| a zod validation error | the validator itself, code 400 |
Set notFound and onError on the main app in src/index.ts: then they also work for the groups mounted with app.route().
Checking
curl -i http://localhost:3000/posts/99The response contains the line HTTP/1.1 404 Not Found and JSON with the message.
Common mistake
Sending the text of any error to the client in onError:
// src/index.ts
app.onError((err, c) => c.json({ error: err.message }, 500))This leaks internal details: SQL query text, file paths, table names. Show the client only the HTTPException messages you wrote yourself, and log the rest.