AI 电子伴侣
创建时间: 2026-04-08最后更新: 2026-04-18

1. 不做错误处理会怎样

先看一个没有任何错误处理的路由:

index.ts
1
app.get('/users/:id', (c) => {
2
const user = findUser(c.req.param('id'))
3
// findUser 返回 undefined,直接访问 .name 会炸
4
return c.json({ name: user.name })
5
})

findUser 返回 undefined 时,代码抛出 TypeError: Cannot read properties of undefined。Hono 默认会返回一个 500 状态码的纯文本响应:

response.txt
1
Internal Server Error

前端拿到的就是这么一行字。没有 JSON,没有错误码,没有任何有用信息。前端开发者没法根据这个做任何有意义的错误提示。

这里先建立一个最重要的判断:

  • 能正常返回结果:用 return c.json(...)
  • 遇到异常情况,需要中断当前流程:用 throw

也就是说,错误处理这篇文章讨论的核心,不是"怎么返回数据",而是"当流程不能继续时,怎么把错误用一种统一、可控的方式抛出去"。

2. HTTPException:Hono 内置的错误类

Hono 提供了 HTTPException,用来抛出带状态码的 HTTP 错误:

index.ts
01
import { Hono } from 'hono'
02
import { HTTPException } from 'hono/http-exception'
03
04
const app = new Hono()
05
06
app.get('/users/:id', (c) => {
07
const user = findUser(c.req.param('id'))
08
if (!user) {
09
throw new HTTPException(404, { message: 'User not found' })
10
}
11
return c.json(user)
12
})

抛出 HTTPException 后,Hono 会把它当成一个"带 HTTP 状态码的错误"来处理。最基础的情况下,它可以直接生成对应状态码的响应。比起让代码自然报错变成 500,这种方式能精确告诉前端「到底出了什么问题」。

HTTPException 的构造函数接收两个参数:

  • 第一个是 HTTP 状态码(400401403404500 等)
  • 第二个是选项对象,message 字段是错误描述

对新手来说,可以先这样理解:HTTPException 就是普通 Error 的 HTTP 版本。普通 Error 只会说"出错了",HTTPException 还会顺便告诉框架"这次应该返回 404 还是 401"。

3. app.onError:全局错误处理器

每个路由里都写 try-catch 太啰嗦。用 app.onError 注册一个全局错误处理器,所有路由里抛出的错误都会走到这里:

index.ts
01
import { Hono } from 'hono'
02
import { HTTPException } from 'hono/http-exception'
03
04
const app = new Hono()
05
06
app.onError((err, c) => {
07
// HTTPException:业务主动抛出的错误,返回对应状态码
08
if (err instanceof HTTPException) {
09
return c.json(
10
{ success: false, error: err.message },
11
err.status
12
)
13
}
14
15
// 其他错误:代码 bug 或未预期的异常
16
console.error(err)
17
return c.json(
18
{ success: false, error: 'Internal Server Error' },
19
500
20
)
21
})

这里顺手补一个认知:app.onError 只会处理抛出来的错误。也就是说:

  • throw new HTTPException(...) 会进入 app.onError
  • return c.json(...) 不会进入 app.onError,因为那已经是一次正常响应了

这也是为什么很多项目会把"业务失败"统一写成 throw,这样所有错误都能汇总到一个地方处理。

这样做的好处:

  • 所有错误响应格式统一,前端只需要处理一种结构
  • HTTPException 和普通异常分开处理,业务错误返回具体信息,代码 bug 返回通用提示(不暴露内部细节)
  • 路由处理函数里只管抛错,不用操心响应格式

4. app.notFound:自定义 404 响应

当请求的路径没有匹配到任何路由时,Hono 默认返回 404 Not Found 纯文本。用 app.notFound 可以自定义这个响应:

index.ts
01
app.notFound((c) => {
02
return c.json(
03
{
04
success: false,
05
error: 'Not Found',
06
path: c.req.path,
07
},
08
404
09
)
10
})

注意 app.notFoundHTTPException(404) 的区别:

  • app.notFound:路径压根没匹配到路由,请求连路由处理函数都没进
  • HTTPException(404):路径匹配到了路由,但业务逻辑判断资源不存在(比如用户 ID 在数据库里查不到)

另外要注意,app.notFound 是给顶层 app 兜底用的。你可以把它理解成"整个应用最后的 404 出口",而不是某个单独路由模块内部的业务判断逻辑。

5. 自定义错误类

实际项目中,光靠 HTTPException 不够细。你可能需要区分认证失败、参数校验失败、权限不足等不同类型的业务错误。

可以继承 HTTPException 做分类:

errors.ts
01
import { HTTPException } from 'hono/http-exception'
02
03
// 认证错误
04
export class AuthError extends HTTPException {
05
constructor(message = 'Unauthorized') {
06
super(401, { message })
07
}
08
}
09
10
// 参数校验错误
11
export class ValidationError extends HTTPException {
12
public details: Record<string, string[]>
13
14
constructor(
15
message: string,
16
details: Record<string, string[]> = {}
17
) {
18
super(400, { message })
19
this.details = details
20
}
21
}
22
23
// 权限不足
24
export class ForbiddenError extends HTTPException {
25
constructor(message = 'Forbidden') {
26
super(403, { message })
27
}
28
}

路由里使用:

index.ts
01
import { AuthError, ValidationError } from './errors'
02
03
app.post('/articles', async (c) => {
04
const token = c.req.header('Authorization')
05
if (!token) {
06
throw new AuthError('Missing token')
07
}
08
09
const body = await c.req.json()
10
if (!body.title) {
11
throw new ValidationError('Validation failed', {
12
title: ['title is required'],
13
})
14
}
15
16
// 正常业务逻辑...
17
return c.json({ success: true, data: body }, 201)
18
})

然后在全局错误处理器里针对不同类型做处理:

index.ts
01
app.onError((err, c) => {
02
if (err instanceof ValidationError) {
03
return c.json(
04
{
05
success: false,
06
error: err.message,
07
code: 'VALIDATION_ERROR',
08
details: err.details,
09
},
10
err.status
11
)
12
}
13
14
if (err instanceof HTTPException) {
15
return c.json(
16
{ success: false, error: err.message },
17
err.status
18
)
19
}
20
21
console.error(err)
22
return c.json(
23
{ success: false, error: 'Internal Server Error' },
24
500
25
)
26
})

注意 ValidationError 的判断要放在 HTTPException 前面,因为 ValidationError 继承自 HTTPException,反过来写的话 instanceof HTTPException 会先匹配到。

6. 统一错误响应格式

把错误响应格式定义清楚,前后端约定好:

types.ts
01
// 统一错误响应结构
02
interface ErrorResponse {
03
success: false
04
error: string
05
code?: string
06
details?: Record<string, string[]>
07
}
08
09
// 统一成功响应结构
10
interface SuccessResponse<T> {
11
success: true
12
data: T
13
}

封装一个工具函数,让响应构造更简洁:

response.ts
01
import type { Context } from 'hono'
02
03
export const success = <T>(c: Context, data: T, status = 200) => {
04
return c.json({ success: true, data }, status)
05
}
06
07
export const error = (
08
c: Context,
09
message: string,
10
status = 400,
11
code?: string
12
) => {
13
return c.json({ success: false, error: message, code }, status)
14
}

这里用普通 number 做状态码参数就够用了。对新手教学来说,这样更直观,也避免一上来引入额外的类型细节。

路由里用起来就很清爽:

index.ts
1
import { success } from './response'
2
3
app.get('/users/:id', (c) => {
4
const user = findUser(c.req.param('id'))
5
if (!user) {
6
throw new HTTPException(404, { message: 'User not found' })
7
}
8
return success(c, user)
9
})

7. 和 zValidator 校验错误的配合

如果你用了 @hono/zod-validator 做请求校验(前面章节介绍过),校验失败默认返回 400 状态码和校验错误详情。但默认格式可能跟你的统一格式不一致。

可以通过 hook 参数把校验失败也接入统一的错误处理:

index.ts
01
import { zValidator } from '@hono/zod-validator'
02
import { z } from 'zod'
03
import { HTTPException } from 'hono/http-exception'
04
05
const createUserSchema = z.object({
06
name: z.string().min(1, 'name is required'),
07
email: z.string().email('invalid email'),
08
})
09
10
app.post(
11
'/users',
12
zValidator('json', createUserSchema, (result, c) => {
13
if (!result.success) {
14
// 校验失败,抛 HTTPException 走全局错误处理
15
throw new HTTPException(400, {
16
message: 'Validation failed',
17
})
18
}
19
}),
20
(c) => {
21
const data = c.req.valid('json')
22
return c.json({ success: true, data }, 201)
23
}
24
)

zValidator 的第三个参数是一个 hook 函数,在校验完成后调用。result.successfalse 时表示校验失败,这时抛出 HTTPException 就能走到 app.onError,保持所有错误响应格式一致。

这个设计背后的思路很值得记住:谁发现错误,谁可以先把错误整理好,再统一抛给全局错误处理器。
这样路由本身就不用关心"校验失败时响应长什么样",它只关心"数据已经合法了,可以继续写业务逻辑"。

如果你想带上具体的字段错误信息,可以用前面定义的 ValidationError

index.ts
01
import { ValidationError } from './errors'
02
03
app.post(
04
'/users',
05
zValidator('json', createUserSchema, (result, c) => {
06
if (!result.success) {
07
const details: Record<string, string[]> = {}
08
result.error.issues.forEach((issue) => {
09
const key = issue.path.join('.')
10
if (!details[key]) details[key] = []
11
details[key].push(issue.message)
12
})
13
throw new ValidationError('Validation failed', details)
14
}
15
}),
16
(c) => {
17
const data = c.req.valid('json')
18
return c.json({ success: true, data }, 201)
19
}
20
)

这样前端收到的校验错误响应长这样:

response.json
1
{
2
"success": false,
3
"error": "Validation failed",
4
"code": "VALIDATION_ERROR",
5
"details": {
6
"name": ["name is required"],
7
"email": ["invalid email"]
8
}
9
}

前端可以直接把 details 里的信息映射到对应的表单字段上,体验非常好。

8. 总结

错误处理的核心就三件事:用 HTTPException 抛业务错误,用 app.onError 统一捕获,用 app.notFound 处理 404。在此基础上,自定义错误类做分类,统一响应格式让前端好对接。

下一篇聊认证与鉴权——怎么用中间件实现 JWT 验证和权限控制。