Python通过sys.excepthook捕获未处理异常,需在启动最早期设置,调用sys.__excepthook__保留原始traceback;Flask中用@app.errorhandler(Exception)统一返回JSON错误响应,区分debug模式,避免状态码错配、日志混淆和序列化崩溃。

Python怎么捕获全局异常:用 sys.excepthook 拦截未处理异常
直接说结论:Python 没有“全局 try-catch”,但可以用 sys.excepthook 捕获所有未被 try/except 拦住的异常,包括主线程崩溃、未捕获的异步异常(需额外处理)、甚至 KeyboardInterrupt(默认不走它,要手动改)。这不是装饰器或中间件,是 Python 解释器级兜底。
常见错误现象:Exception ignored in: ... 日志满屏飞,但你完全没看到堆栈;或者服务突然退出,日志里只有半截 traceback。这时候说明异常漏过了所有 try,必须靠 sys.excepthook 补上最后一道防线。
实操建议:
- 在程序启动最早期(比如
if __name__ == "__main__":下第一行)就设置sys.excepthook,晚了可能错过初始化阶段的异常 - 不要在 hook 里 raise 新异常,否则会触发递归调用,最终进程强制终止
- 务必调用
sys.__excepthook__原始函数打印原始 traceback(除非你完全自定义输出格式),否则会丢失关键上下文
import sys
def global_exception_handler(exc_type, exc_value, exc_traceback):
# 记日志、发告警、写监控指标...
print(f"[FATAL] Uncaught {exc_type.__name__}: {exc_value}")
# 保留原 traceback 输出
sys.__excepthook__(exc_type, exc_value, exc_traceback)
<p>sys.excepthook = global_exception_handler
Web 框架里怎么统一返回标准错误响应:以 Flask 为例
Flask 的 @app.errorhandler 只管 HTTP 异常(HTTPException 子类)和你显式 raise 的特定异常,对 ValueError、KeyError 这类非 HTTP 异常默认 500 且响应体是 HTML(开发模式)或空(生产模式),根本不是你想要的 JSON 格式。
立即学习“Python免费学习笔记(深入)”;
实操建议:
- 注册
@app.errorhandler(Exception)捕获所有未被更具体 handler 拦住的异常,这是最常用也最有效的统一入口 - 区分开发/生产环境:开发时保留完整 traceback(方便调试),生产时只返回错误码和简短消息,避免信息泄露
- 注意:这个 handler 不会捕获后台线程、信号处理函数、或
atexit里的异常,那些仍需sys.excepthook
@app.errorhandler(Exception)
def handle_unexpected_error(e):
status_code = 500
response = {
"code": "INTERNAL_ERROR",
"message": str(e) if app.debug else "Internal server error"
}
return jsonify(response), status_code
为什么 try/except Exception 在函数里不够用
很多人以为在每个路由函数里加一层 try/except Exception 就能搞定,结果发现中间件、装饰器、数据库连接池初始化、甚至 __init__.py 导入时抛的异常都漏掉了。根本原因是:异常发生位置不在你的函数执行栈里。
使用场景举例:
- SQLAlchemy 初始化时连接失败(
create_engine抛OperationalError)—— 发生在app = Flask(...)之后、第一个请求之前 - 某个
@before_first_request函数里读配置文件失败 —— 请求还没进路由,try根本没机会包到 - 异步任务(Celery、threading)里抛异常 —— 它们有自己的线程/进程,主应用的
try看不见
所以单靠函数内 try 是碎片化防御,sys.excepthook + 框架级 errorhandler 才构成真正覆盖全生命周期的异常拦截网。
统一响应格式容易踩的坑:状态码、日志、JSON 序列化
统一响应不只是把 message 包进 JSON,三个地方最容易出问题:
-
status_code错配:比如把400 Bad Request写成200 OK,前端判断逻辑全乱;又或者把数据库连接失败硬塞进503 Service Unavailable,其实该用500 - 日志级别混淆:所有异常都打
logging.error,导致真正需要人工介入的故障被淹没;应该按异常类型分error/warning,比如ValidationError打warning,ConnectionRefusedError才打error - JSON 序列化失败:异常对象本身含不可序列化字段(如
datetime、Decimal、自定义类实例),直接jsonify({"error": e})会再抛一次TypeError,导致二次崩溃
复杂点在于:异常源头可能来自底层库(比如 psycopg2 报错带 connection 对象引用),而你根本没法控制它的 __str__ 行为。这时候得做清洗,不能无脑 str(e)。










