Files
gh-jezweb-claude-skills-ski…/templates/error-handling.ts
2025-11-30 08:24:59 +08:00

410 lines
10 KiB
TypeScript

/**
* 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,
}