异常捕获应以“可恢复的最小业务单元”为边界,如一次支付或下单流程;避免为单行转换操作单独try,禁用except:和except exception:,自定义异常需分层且以error结尾,捕获后须做状态清理或补偿。

try 块该包多大?按业务步骤,不是按代码行数
异常捕获不是越细越好,也不是图省事一把全包——关键看哪一步失败后你还能“兜得住”。比如用户下单,扣库存、生成订单、发MQ消息这三步是一体的,中间任何一环失败都得整体回滚;这时候把整个流程包进一个 try,统一捕获 InventoryError 或 MessageSendError,比在每行 redis.decr() 前都加个 try-except 强得多。
- ✅ 推荐:以“可恢复的最小业务单元”为边界,比如一次支付、一次文件上传、一次配置加载
- ❌ 避免:为
json.loads()、int()、dict.get()单独套try——它们本就该用更轻量方式处理(如try/except ValueError只包这一句,不连带后续逻辑) - ⚠️ 风险:粒度太碎会让错误被静默吞掉,日志里只看到“某处报了
KeyError”,但不知道它发生在下单前还是发货后
为什么不能写 except: 或 except Exception:?
因为它们会吃掉你不该拦的信号。比如用户按 Ctrl+C 想中断脚本,except: 会把它当成普通异常吞掉,程序卡住不动;而 except Exception: 虽放行 KeyboardInterrupt,却仍会拦截 SystemExit,导致 sys.exit() 失效——这在 CLI 工具或容器健康检查里是致命的。
- ✅ 安全写法:
except (ValueError, OSError, requests.exceptions.Timeout)—— 明确知道要处理什么 - ✅ 真需要兜底?只在最外层日志/清理场景用
except BaseException:,且必须if isinstance(e, (KeyboardInterrupt, SystemExit)):再raise - ❌
except:在任何生产代码里都不该出现,它等价于except BaseException:,风险极高
自定义异常怎么抛、怎么接?底层具体,上层策略化
不要让数据库模块返回 None 表示查不到用户,也不要让 API 层自己判断 if resp.status_code == 404。异常要分层:底层暴露细节,上层决定怎么应对。
- ✅ 底层(如 DAO)抛
UserNotFoundError、DatabaseConnectionError—— 类型明确,不模糊 - ✅ 上层(如 FastAPI 路由)捕获
UserNotFoundError返回HTTP_404,捕获DatabaseConnectionError返回HTTP_503并触发告警 - ⚠️ 注意:自定义异常类名必须以
Error结尾(如InsufficientBalanceError),继承Exception,别直接继承BaseException - ? 小技巧:模块内先定义一个
BaseAppError,其他业务异常都继承它,方便上层统一except BaseAppError:做基础日志
捕获之后,状态清理和补偿比打印日志更重要
catch 到异常不等于问题结束。你要立刻问自己:刚才那几行代码,有没有留下“半成品”?文件写了一半?数据库插入了订单但没扣库存?消息发出去但没收到 ACK?
立即学习“Python免费学习笔记(深入)”;
- ✅ 优先用
with管理资源:文件、数据库连接、锁,确保finally或上下文退出时自动释放 - ✅ 多步操作考虑幂等性:比如订单号带时间戳+随机数,重复提交不会创建两单;或者失败时写入延迟队列,由后台重试
- ⚠️ 不要只写
logging.exception("oops")就完事——得补一句rollback_transaction()或delete_temp_file(),否则下次运行可能踩进同一个坑
粒度控制最难的地方不在语法,而在对业务副作用的预判。同一段代码,在定时任务里可能要强清理,在 Web 请求里可能要快速降级,在 CLI 工具里可能得精确提示用户哪里填错了——没有银弹,只有根据上下文做取舍。










