Skip to main content

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:
  1. Context (c): Provides access to request/response data and utilities
  2. 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)
    }
  })
})

Build docs developers (and LLMs) love