自定义错误码必须修改 render() 而非 report();render() 决定 API 返回内容,需统一 JSON 格式、分离业务 code 与 HTTP 状态码、所有分支必须 return 响应,且禁止在 render() 中执行耗时操作。

自定义错误码必须改 render(),不是改 report()
很多人以为在 App\Exceptions\Handler::report() 里加逻辑就能控制返回内容,其实 report() 只负责日志记录和上报,真正决定 API 返回什么的是 render()。Laravel 默认的 render() 对 ValidationException、ModelNotFoundException 等有内置处理,但它们返回的 HTTP 状态码和结构不统一,比如 422 错误带 errors 字段,而 404 却只返回空响应体。
实操建议:
- 在
App\Exceptions\Handler::render()中拦截常见异常,用response()->json()统一格式返回 - 对
ValidationException提取$exception->errors(),不要直接用$exception->getMessage() - 对业务抛出的
CustomException(如InsufficientBalanceException),提前约定好getCode()返回数字错误码,getMessage()返回用户提示语 - 别在
render()里做耗时操作(如 DB 查询、HTTP 调用),它在请求生命周期末尾执行,容易拖慢响应
Laravel 10+ 的 render() 必须显式 return,否则报错
从 Laravel 10 开始,render() 方法签名强制要求返回 \Illuminate\Http\Response|\Symfony\Component\HttpFoundation\Response。如果你写了逻辑但忘了 return,或者只写了 dd() / Log::debug() 就结束函数,会触发 TypeError: Return value of App\Exceptions\Handler::render() must be an instance of...。
常见错误现象:
- 开发环境能看到错误页,但生产环境返回 500 且无日志(因为
render()自身崩了) - API 响应体为空,状态码却是 200(PHP 默认返回空响应)
实操建议:
- 所有分支路径都必须有
return,包括if ($e instanceof ...)和最后的return parent::render($request, $e); - 用 IDE 的「未覆盖分支检查」或 PHPStan 配置 strict-return 检查
- 测试时用
throw new CustomException('msg', 40001)手动触发,别只靠前端报错才验证
统一返回结构里 code 别直接用 HTTP 状态码
HTTP 状态码(如 400、422、500)是协议层语义,而业务错误码(如 1001、2003、9999)是领域语义,混用会导致前端难处理:比如 422 可能对应参数缺失、手机号格式错、邮箱已注册三种完全不同的 UI 提示,光看状态码无法区分。
使用场景:
- 前端需要根据
code做精确 toast 提示或跳转(如code === 40002→ 弹登录框) - 运维监控按
code聚合告警,而不是按 HTTP 状态码(避免把所有 422 当一类问题)
实操建议:
-
code字段固定为整数,由后端集中定义在app/Exceptions/ErrorCode.php常量类中 - HTTP 状态码仍按规范设(4xx 用 400/422/404,5xx 用 500),和
code解耦 - 不要把数据库错误码(如 MySQL 的 1062)直接透传给前端,需映射为业务码
中间件里 throw 异常比手动 return 更可靠
有人喜欢在中间件里写 return response()->json([...], 401),看似简单,但绕过了 Laravel 的异常处理流程:日志不会被 report() 捕获、Sentry 不会上报、render() 的统一逻辑也不生效。
性能与兼容性影响:
- 手动
return会让中间件提前终止 pipeline,后续中间件(如日志、性能追踪)不再执行 - 某些包(如
laravel/sanctum)依赖异常冒泡机制来清理 session 或 token,直接 return 会破坏其行为
实操建议:
- 在中间件中统一
throw new AuthenticationException('token expired', 40001) - 让
render()统一处理所有AuthenticationException子类,保持结构一致 - 如果真要短路响应(比如风控拦截),也建议封装成
AbortException并在render()中特殊处理,而非裸写return
最易被忽略的一点:错误码的语义一致性得靠团队约束,不是靠代码强制。哪怕你把 ErrorCode 类写得再规范,只要某人临时加了个 throw new Exception('xxx', 8888),整个体系就松动了。上线前扫一遍 throw new Exception 和 throw new \Exception 是必要的。


















