/** * Hono Error Handling * * Complete examples for error handling using HTTPException, onError, and custom error handlers. */ import { Hono } from 'hono' import { HTTPException } from 'hono/http-exception' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' const app = new Hono() // ============================================================================ // HTTPEXCEPTION - CLIENT ERRORS (400-499) // ============================================================================ // 400 Bad Request app.get('/bad-request', (c) => { throw new HTTPException(400, { message: 'Bad Request - Invalid parameters' }) }) // 401 Unauthorized app.get('/unauthorized', (c) => { throw new HTTPException(401, { message: 'Unauthorized - Missing or invalid token' }) }) // 403 Forbidden app.get('/forbidden', (c) => { throw new HTTPException(403, { message: 'Forbidden - Insufficient permissions' }) }) // 404 Not Found app.get('/users/:id', async (c) => { const id = c.req.param('id') // Simulate database lookup const user = null // await db.findUser(id) if (!user) { throw new HTTPException(404, { message: `User with ID ${id} not found` }) } return c.json({ user }) }) // Custom response body app.get('/custom-error', (c) => { const res = new Response( JSON.stringify({ error: 'CUSTOM_ERROR', code: 'ERR001', details: 'Custom error details', }), { status: 400, headers: { 'Content-Type': 'application/json', }, } ) throw new HTTPException(400, { res }) }) // ============================================================================ // AUTHENTICATION ERRORS // ============================================================================ app.get('/protected', (c) => { const token = c.req.header('Authorization') if (!token) { throw new HTTPException(401, { message: 'Missing Authorization header', }) } if (!token.startsWith('Bearer ')) { throw new HTTPException(401, { message: 'Invalid Authorization header format', }) } const actualToken = token.replace('Bearer ', '') if (actualToken !== 'valid-token') { throw new HTTPException(401, { message: 'Invalid or expired token', }) } return c.json({ message: 'Access granted', data: 'Protected data' }) }) // ============================================================================ // VALIDATION ERRORS // ============================================================================ const userSchema = z.object({ name: z.string().min(1), email: z.string().email(), age: z.number().int().min(18), }) // Validation errors automatically return 400 app.post('/users', zValidator('json', userSchema), (c) => { const data = c.req.valid('json') return c.json({ success: true, data }) }) // Custom validation error handler app.post( '/users/custom', zValidator('json', userSchema, (result, c) => { if (!result.success) { throw new HTTPException(400, { message: 'Validation failed', cause: result.error, }) } }), (c) => { const data = c.req.valid('json') return c.json({ success: true, data }) } ) // ============================================================================ // GLOBAL ERROR HANDLER (onError) // ============================================================================ app.onError((err, c) => { console.error(`Error on ${c.req.method} ${c.req.path}:`, err) // Handle HTTPException if (err instanceof HTTPException) { // Get custom response if provided if (err.res) { return err.res } // Return default HTTPException response return c.json( { error: err.message, status: err.status, }, err.status ) } // Handle Zod validation errors if (err.name === 'ZodError') { return c.json( { error: 'Validation failed', issues: err.issues, }, 400 ) } // Handle unexpected errors (500) return c.json( { error: 'Internal Server Error', message: process.env.NODE_ENV === 'development' ? err.message : 'An unexpected error occurred', }, 500 ) }) // ============================================================================ // NOT FOUND HANDLER // ============================================================================ app.notFound((c) => { return c.json( { error: 'Not Found', message: `Route ${c.req.method} ${c.req.path} not found`, }, 404 ) }) // ============================================================================ // MIDDLEWARE ERROR CHECKING // ============================================================================ app.use('*', async (c, next) => { await next() // Check for errors after handler execution if (c.error) { console.error('Error detected in middleware:', { error: c.error.message, path: c.req.path, method: c.req.method, }) // Send to error tracking service // await sendToSentry(c.error, { path: c.req.path, method: c.req.method }) } }) // ============================================================================ // TRY-CATCH ERROR HANDLING // ============================================================================ app.get('/external-api', async (c) => { try { // Simulated external API call const response = await fetch('https://api.example.com/data') if (!response.ok) { throw new HTTPException(response.status, { message: `External API returned ${response.status}`, }) } const data = await response.json() return c.json({ data }) } catch (error) { // Network errors or parsing errors if (error instanceof HTTPException) { throw error // Re-throw HTTPException } // Log unexpected error console.error('External API error:', error) // Return generic error to client throw new HTTPException(503, { message: 'External service unavailable', }) } }) // ============================================================================ // CONDITIONAL ERROR RESPONSES // ============================================================================ app.get('/data/:id', async (c) => { const id = c.req.param('id') // Validate ID format if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) { throw new HTTPException(400, { message: 'Invalid UUID format', }) } // Check access permissions const hasAccess = true // await checkUserAccess(id) if (!hasAccess) { throw new HTTPException(403, { message: 'You do not have permission to access this resource', }) } // Fetch data const data = null // await db.getData(id) if (!data) { throw new HTTPException(404, { message: 'Resource not found', }) } return c.json({ data }) }) // ============================================================================ // TYPED ERROR RESPONSES // ============================================================================ type ErrorResponse = { error: string code: string details?: string } function createErrorResponse(code: string, message: string, details?: string): ErrorResponse { return { error: message, code, details, } } app.get('/typed-error', (c) => { const errorBody = createErrorResponse('USER_NOT_FOUND', 'User not found', 'The requested user does not exist') throw new HTTPException(404, { res: c.json(errorBody, 404), }) }) // ============================================================================ // CUSTOM ERROR CLASSES // ============================================================================ class ValidationError extends HTTPException { constructor(message: string, cause?: any) { super(400, { message, cause }) this.name = 'ValidationError' } } class AuthenticationError extends HTTPException { constructor(message: string) { super(401, { message }) this.name = 'AuthenticationError' } } class AuthorizationError extends HTTPException { constructor(message: string) { super(403, { message }) this.name = 'AuthorizationError' } } class NotFoundError extends HTTPException { constructor(resource: string) { super(404, { message: `${resource} not found` }) this.name = 'NotFoundError' } } // Usage app.get('/custom-errors/:id', async (c) => { const id = c.req.param('id') if (!id) { throw new ValidationError('ID is required') } const user = null // await db.findUser(id) if (!user) { throw new NotFoundError('User') } return c.json({ user }) }) // Handle custom error classes in onError app.onError((err, c) => { if (err instanceof ValidationError) { return c.json({ error: err.message, type: 'validation' }, err.status) } if (err instanceof AuthenticationError) { return c.json({ error: err.message, type: 'authentication' }, err.status) } if (err instanceof NotFoundError) { return c.json({ error: err.message, type: 'not_found' }, err.status) } if (err instanceof HTTPException) { return err.getResponse() } return c.json({ error: 'Internal Server Error' }, 500) }) // ============================================================================ // ERROR LOGGING WITH CONTEXT // ============================================================================ app.use('*', async (c, next) => { const requestId = crypto.randomUUID() c.set('requestId', requestId) try { await next() } catch (error) { // Log with context console.error('Request failed', { requestId, method: c.req.method, path: c.req.path, error: error instanceof Error ? error.message : 'Unknown error', stack: error instanceof Error ? error.stack : undefined, }) // Re-throw to be handled by onError throw error } }) // ============================================================================ // EXPORT // ============================================================================ export default app export { ValidationError, AuthenticationError, AuthorizationError, NotFoundError, createErrorResponse, }