Overview
Handlers are functions that process HTTP requests and return responses. In Hono, every route is associated with one or more handler functions.
Handler Types
Hono defines two main handler types:
Handler
A standard request handler that returns a response:
From src/types.ts:76-81:
type Handler<
E extends Env = any,
P extends string = any,
I extends Input = BlankInput,
R extends HandlerResponse<any> = any,
> = (c: Context<E, P, I>, next: Next) => R
MiddlewareHandler
A middleware-style handler that must call next() or return a response:
From src/types.ts:83-88:
type MiddlewareHandler<
E extends Env = any,
P extends string = string,
I extends Input = {},
R extends HandlerResponse<any> = Response,
> = (c: Context<E, P, I>, next: Next) => Promise<R | void>
Combined Handler Type
The H type represents either a Handler or MiddlewareHandler:
From src/types.ts:90-95:
type H<
E extends Env = any,
P extends string = any,
I extends Input = BlankInput,
R extends HandlerResponse<any> = any,
> = Handler<E, P, I, R> | MiddlewareHandler<E, P, I, R>
Handler Signature
All handlers receive two parameters:
- Context (
c): Provides access to request/response data and utilities
- Next (
next): Function to pass control to the next handler
import { Context } from 'hono'
import type { Next } from 'hono'
app.get('/example', async (c: Context, next: Next) => {
// Handler logic
return c.json({ message: 'Hello' })
})
Handler Response Types
Handlers can return various response types:
From src/types.ts:70-74:
type HandlerResponse<O> =
| Response
| TypedResponse<O>
| Promise<Response | TypedResponse<O>>
| Promise<void>
Returning Responses
Synchronous Response
app.get('/sync', (c) => {
return c.text('Synchronous response')
})
Asynchronous Response
app.get('/async', async (c) => {
const data = await fetchData()
return c.json(data)
})
Raw Response Object
app.get('/raw', (c) => {
return new Response('Raw response', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
})
Basic Handler Examples
Simple GET Handler
app.get('/hello', (c) => {
return c.text('Hello, World!')
})
POST Handler with JSON
app.post('/users', async (c) => {
const body = await c.req.json()
const user = { id: 1, ...body }
return c.json(user, 201)
})
Handler with Path Parameters
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ userId: id })
})
Handler with Query Parameters
app.get('/search', (c) => {
const query = c.req.query('q')
const page = c.req.query('page') || '1'
return c.json({ query, page })
})
Multiple Handlers
Routes can have multiple handlers that execute in sequence:
const authenticate = async (c, next) => {
const token = c.req.header('Authorization')
if (!token) {
return c.text('Unauthorized', 401)
}
c.set('authenticated', true)
await next()
}
const logRequest = async (c, next) => {
console.log(`${c.req.method} ${c.req.path}`)
await next()
}
app.get('/protected',
logRequest,
authenticate,
(c) => {
return c.json({ message: 'Protected data' })
}
)
Type-Safe Handlers
Define environment and input types for type safety:
import { Hono } from 'hono'
import type { Context } from 'hono'
type Env = {
Bindings: {
DB: D1Database
}
Variables: {
user: { id: string; name: string }
}
}
const app = new Hono<Env>()
app.get('/users', async (c: Context<Env>) => {
const db = c.env.DB // Type: D1Database
const user = c.get('user') // Type: { id: string; name: string }
const users = await db.prepare('SELECT * FROM users').all()
return c.json(users)
})
Error Handling
Throwing Errors
Handlers can throw errors that are caught by the error handler:
import { HTTPException } from 'hono/http-exception'
app.get('/users/:id', async (c) => {
const id = c.req.param('id')
const user = await findUser(id)
if (!user) {
throw new HTTPException(404, { message: 'User not found' })
}
return c.json(user)
})
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse()
}
return c.text('Internal Server Error', 500)
})
Try-Catch in Handlers
app.post('/data', async (c) => {
try {
const body = await c.req.json()
const result = await processData(body)
return c.json(result)
} catch (error) {
console.error('Processing failed:', error)
return c.json({ error: 'Failed to process data' }, 400)
}
})
Error Handler Types
From src/types.ts:113-119:
export interface HTTPResponseError extends Error {
getResponse: () => Response
}
export type ErrorHandler<E extends Env = any> = (
err: Error | HTTPResponseError,
c: Context<E>
) => Response | Promise<Response>
Async Handlers
All async operations should use async/await:
app.get('/async-data', async (c) => {
// Parallel requests
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
])
return c.json({ users, posts })
})
app.post('/upload', async (c) => {
const formData = await c.req.formData()
const file = formData.get('file')
if (!file) {
return c.text('No file uploaded', 400)
}
await saveFile(file)
return c.json({ message: 'File uploaded' })
})
Streaming Responses
Handlers can return streaming responses:
app.get('/stream', (c) => {
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
await sleep(100)
controller.enqueue(`chunk ${i}\n`)
}
controller.close()
}
})
return c.body(stream)
})
app.get('/sse', (c) => {
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder()
for (let i = 0; i < 5; i++) {
await sleep(1000)
const data = `data: ${JSON.stringify({ count: i })}\n\n`
controller.enqueue(encoder.encode(data))
}
controller.close()
}
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
})
})
Handler Registration
From src/hono-base.ts:385-391, handlers are added to routes:
#addRoute(method: string, path: string, handler: H): void {
method = method.toUpperCase()
path = mergePath(this._basePath, path)
const r: RouterRoute = { basePath: this._basePath, path, method, handler }
this.router.add(method, path, [handler, r])
this.routes.push(r)
}
Handler Execution
From src/hono-base.ts:423-442, handlers are executed:
// Do not `compose` if it has only one handler
if (matchResult[0].length === 1) {
let res: ReturnType<H>
try {
res = matchResult[0][0][0][0](c, async () => {
c.res = await this.#notFoundHandler(c)
})
} catch (err) {
return this.#handleError(err, c)
}
return res instanceof Promise
? res
.then(
(resolved: Response | undefined) =>
resolved || (c.finalized ? c.res : this.#notFoundHandler(c))
)
.catch((err: Error) => this.#handleError(err, c))
: (res ?? this.#notFoundHandler(c))
}
Handler Context Passing
Pass data between handlers using context variables:
const setTimestamp = async (c, next) => {
c.set('timestamp', Date.now())
await next()
}
const logDuration = async (c, next) => {
await next()
const start = c.get('timestamp')
const duration = Date.now() - start
console.log(`Request took ${duration}ms`)
}
app.get('/timed',
setTimestamp,
logDuration,
(c) => {
return c.text('Done')
}
)
Not Found Handler
Customize the 404 handler:
app.notFound((c) => {
return c.json(
{
message: 'Not Found',
path: c.req.path
},
404
)
})
From src/types.ts:107-111:
export type NotFoundHandler<E extends Env = any> = (
c: Context<E>
) => NotFoundResponse extends Response
? NotFoundResponse | Promise<NotFoundResponse>
: Response | Promise<Response>
Custom Error Handler
Define a global error handler:
app.onError((err, c) => {
console.error(`${err}`)
if (err instanceof HTTPException) {
const response = err.getResponse()
return c.json(
{ error: err.message },
response.status as any
)
}
return c.json(
{ error: 'Internal Server Error' },
500
)
})
From src/hono-base.ts:35-42:
const errorHandler: ErrorHandler = (err, c) => {
if ('getResponse' in err) {
const res = err.getResponse()
return c.newResponse(res.body, res)
}
console.error(err)
return c.text('Internal Server Error', 500)
}
Best Practices
- Always return a response from handlers
- Use
async/await for asynchronous operations
- Throw
HTTPException for HTTP errors with status codes
- Use TypeScript to define environment and variable types
- Keep handlers focused on a single responsibility
- Use middleware for cross-cutting concerns
Handlers should always return a Response object. If a handler doesn’t return anything, Hono will throw an error: “Context is not finalized. Did you forget to return a Response object?”
Common Patterns
CRUD Operations
// Create
app.post('/users', async (c) => {
const user = await c.req.json()
const created = await db.createUser(user)
return c.json(created, 201)
})
// Read
app.get('/users/:id', async (c) => {
const id = c.req.param('id')
const user = await db.getUser(id)
return user ? c.json(user) : c.notFound()
})
// Update
app.put('/users/:id', async (c) => {
const id = c.req.param('id')
const updates = await c.req.json()
const updated = await db.updateUser(id, updates)
return c.json(updated)
})
// Delete
app.delete('/users/:id', async (c) => {
const id = c.req.param('id')
await db.deleteUser(id)
return c.body(null, 204)
})
File Upload Handler
app.post('/upload', async (c) => {
const formData = await c.req.formData()
const file = formData.get('file') as File
if (!file) {
return c.json({ error: 'No file provided' }, 400)
}
const arrayBuffer = await file.arrayBuffer()
const saved = await saveToStorage(arrayBuffer, file.name)
return c.json({
message: 'File uploaded',
filename: file.name,
size: file.size,
url: saved.url
})
})
Pagination Handler
app.get('/items', async (c) => {
const page = parseInt(c.req.query('page') || '1')
const limit = parseInt(c.req.query('limit') || '10')
const offset = (page - 1) * limit
const [items, total] = await Promise.all([
db.getItems(limit, offset),
db.countItems()
])
return c.json({
items,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
})
})