异常捕获别乱写!一个裸的 except: 让我调试了通宵,这3种写法差距太大了

凌晨三点的生产事故

那天晚上,我刚躺下,手机就炸了。

运维老张在群里连着发了十几条消息:"支付接口超时率飙升到30%了,订单全卡住了,你在吗?"

我瞬间清醒。上个月刚上线的支付模块,出了问题。

远程连上服务器,翻日志。

奇怪的事情发生了------日志里什么都没有。没有报错,没有异常堆栈,只有一条条"支付失败"的业务记录。

我把代码翻来覆去看了三遍,逻辑没问题啊。

再往下翻,看到了这段:

sql 复制代码
try:
    result = third_party_pay(amount, card_info)
    save_order(result)
except:
    log.error("支付失败")
    return False

就这么一段。看起来还挺"安全"的------出错了就记日志、返回失败,挺规范吧?

但就是这个看似"安全"的写法,把我坑得通宵没睡。

因为 except: 这个裸捕获,把所有异常 都吞掉了。包括 KeyboardInterruptSystemExit,也包括最要命的------内存不足时的 MemoryError

支付接口调用失败了,但失败的真实原因------对方接口返回了一个特殊格式的异常,我根本没捕获到。那个异常里面,其实带着"银行卡已过期"的具体原因。

except: 不管三七二十一,全吞了。我只看到"支付失败"四个字,然后去查日志,日志里什么详细信息都没有。

那个通宵,我什么都没干成。就因为多写了三个字符------: 前面没有指定异常类型。

天亮的时候,我把那段代码改成了这样:

python 复制代码
try:
    result = third_party_pay(amount, card_info)
    save_order(result)
except PaymentTimeoutError:
    log.error("支付超时,稍后重试")
except InvalidCardError as e:
    log.error(f"卡号无效:{e.detail}")
    return False
except Exception as e:
    log.error(f"未知支付异常:{e}")
    log.exception(e)
    return False

问题当场定位:卡号格式不对。用户输错了一个数字。前端校验没拦住,后端裸 except 又给吞了。

从那天起,我发誓再也不写裸 except 了。

裸 except 到底错在哪里?

来认识一下Python里三种最常见的异常捕获写法:

python 复制代码
# 写法一:裸 except(最危险)
try:
    do_something()
except:
    print("出错了")

# 写法二:捕获 Exception(比较安全)
try:
    do_something()
except Exception:
    print("出错了")

# 写法三:捕获具体异常(最推荐)
try:
    do_something()
except ValueError:
    print("值错了")

表面上看,写法一和写法二好像差不多------都是"出错了就执行"。

但差太多了。

关键在于:Python的异常体系是分层的

php 复制代码
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
    ├── ValueError
    ├── TypeError
    ├── AttributeError
    ├── KeyError
    ├── RuntimeError
    └── ... (几十种内置异常)

except: 捕获的是所有继承自 BaseException 的异常

except Exception: 捕获的是所有继承自 Exception 的异常

区别在哪?SystemExitKeyboardInterruptGeneratorExit 这三个直接继承自 BaseException不继承自 Exception

什么意思?

  • KeyboardInterrupt 是你按 Ctrl+C 时抛出的异常
  • SystemExitsys.exit() 时抛出的异常

如果你的代码里用了裸 except,按 Ctrl+C 都退不出去------因为异常被吞了,程序会继续执行。

这不是开玩笑。我曾经见过一个脚本,因为写了个裸 except,运维按了半天 Ctrl+C 都关不掉,最后只能 kill -9 强杀进程。

吞掉异常的真实代价

except 最大的问题不是捕获了什么,而是你根本不知道捕获了什么

看这段代码:

ini 复制代码
try:
    result = json.loads(response.text)
except:
    result = {}

看起来挺合理------解析JSON,失败了就用空字典。

但如果 responseNoneresponse.text 会抛出 AttributeError。被 except 吞了。

如果 response.text""json.loads("") 会抛出 JSONDecodeError。又被吞了。

如果内存不够,抛出 MemoryError。照样被吞。

你拿到一个空字典,然后继续往下执行。后续的业务逻辑就建立在"这是个空字典"的假设上。

你调试的时候,看到的不是 JSONDecodeError,而是一堆莫名其妙的 KeyErrorTypeError

真正的异常被吞掉了,吐出来的是一个面目全非的"症状"。

这就是为什么我那个通宵什么都没查到------真实异常被吞了,日志里只有四个字"支付失败"。

三种正确的写法

写法一:捕获具体异常

python 复制代码
try:
    with open('config.json') as f:
        config = json.load(f)
except FileNotFoundError:
    print("配置文件不存在,使用默认配置")
    config = DEFAULT_CONFIG
except json.JSONDecodeError as e:
    print(f"配置文件格式错误:{e}")
    raise

这是最推荐的写法------你明确知道可能出什么错,而且每种错误都有对应的处理方式

写法二:捕获 Exception 并记录详细信息

当你确实需要捕获"所有可能的异常"时(比如在框架的顶层),用 except Exception 而不是裸 except

python 复制代码
try:
    result = call_external_service()
except Exception as e:
    # 记录完整的堆栈信息
    log.error(f"调用外部服务失败:{e}")
    log.exception(e)  # 这行会把完整堆栈打出来
    # 返回一个安全的默认值,或者降级处理
    return DEFAULT_RESULT

关键点:捕获了,就一定要记录。 而且要用 log.exception 或者 traceback.format_exc(),把堆栈信息留下来。

写法三:捕获特定异常并重新抛出

有时候你捕获异常不是为了处理它,而是为了在抛出去之前做点事情------比如记录日志、清理资源:

python 复制代码
try:
    result = process_data()
except ValueError as e:
    log.warning(f"数据异常:{e}")
    # 做完清理工作后,重新抛出
    raise
except RuntimeError as e:
    log.error(f"运行时错误:{e}")
    # 这个太严重了,直接转成业务异常
    raise BusinessError("系统繁忙,请稍后重试") from e

注意 raise 不加参数表示重新抛出当前异常,不改变堆栈。

还有两个容易被忽略的细节

细节一:except 是有顺序的

python 复制代码
try:
    do_something()
except Exception:
    print("兜底")
except ValueError:
    print("值错误")  # 这行永远不会执行

因为 ExceptionValueError 的父类,except Exception 在前面就把所有 ValueError 都截胡了。

正确的顺序是:具体的在前,通用的在后。

python 复制代码
try:
    do_something()
except ValueError:
    print("值错误")
except TypeError:
    print("类型错误")
except Exception:
    print("其他错误")

细节二:elsefinally 的妙用

try 后面可以跟 elsefinally

python 复制代码
try:
    result = risky_operation()
except ValueError:
    log.error("值异常")
else:
    # 只有没有异常时才会执行
    log.info("操作成功,结果是:{result}")
finally:
    # 不管有没有异常,都会执行
    cleanup_resources()

else 适合放"一切正常时才做"的事情,finally 适合放"无论如何都要做"的事情------比如关闭文件、释放锁。

注意:如果你在 exceptreturn 了,finally 还是会执行。 finally 是真正意义上的"无论如何都会执行"。

实战建议:什么时候用什么

写工具函数、库代码时: 抛出具体的异常,不要捕获。让调用者决定怎么处理。

python 复制代码
def parse_config(path):
    with open(path) as f:
        return json.load(f)
    # 不捕获异常,让调用者自己去处理

写业务逻辑时: 捕获具体异常,做针对性处理。实在需要兜底,用 except Exception 并记录日志。

写框架、中间件、顶层入口时: 可以有一个 except Exception 兜底,但必须记录完整堆栈

css 复制代码
def main():
    try:
        run_app()
    except Exception as e:
        log.critical(f"程序崩溃:{e}")
        log.exception(e)
        sys.exit(1)

现在你再看那三种写法

回过头来,这三种写法差距到底有多大?

makefile 复制代码
# 写法一:裸 except
except:
    # 捕获所有异常,包括 Ctrl+C
    # 你知道捕获了什么吗?不知道。
    # 等于在代码里埋了一颗地雷。
php 复制代码
# 写法二:except Exception
except Exception:
    # 捕获普通异常,但不影响 Ctrl+C
    # 至少不会让你关不掉程序
    # 但还是太宽泛,建议配合日志使用
python 复制代码
# 写法三:except ValueError
except ValueError:
    # 只捕获特定异常
    # 你知道发生了什么,也知道该怎么处理
    # 其他异常会正常上抛,不会遗漏

从"埋雷"到"专业",差的只是一个异常类型名。

那天之后

那个通宵之后,我给自己定了一条铁律:

永远不写裸 except

如果实在想写,先停一下,问自己三个问题:

  1. 这里可能会抛出哪些异常?
  2. 每种异常我打算怎么处理?
  3. 万一有我没想到的异常,我怎么保证不丢失信息?

想清楚了再下笔。

异常捕获不是为了"让程序不崩溃"------而是让程序在崩溃的时候,你能立刻知道为什么崩溃,并且用最快的速度修好它。

那个通宵,我损失了睡眠,但换来了一个教训。

值了。

相关推荐
用户4279254051712 小时前
weibo-cli 实战:用命令行搭建微博自动化运营 Pipeline
后端
Oneslide2 小时前
kibana APM监控面板指标解析-I-JVM相关指标
后端
我不是AI2 小时前
Codex 装好了却用不了?API Key 与 KKFlow 配置精简教程
后端
用户208046804562 小时前
Flask 请求与响应新手实战指南
后端
程序员cxuan2 小时前
A 社官方:我们删掉了 80% 的 skills
人工智能·后端·程序员
苍何2 小时前
AI 短剧出海,门槛已经低到离谱了
后端
程序员黑豆2 小时前
鸿蒙应用开发:@Link 装饰器实现父子组件双向同步
前端·后端·harmonyos
huahailing10243 小时前
Spring Boot 集成 XXL-Job 完整实现方案(支持动态CRUD)
java·spring boot·后端
顶级自由人3 小时前
【前端菜鸟的补课01】Zod 与 PostgreSQL 全栈数据工程教学
前端·后端·程序员