异常与堆栈追踪:从懵圈到精准定位

说实话,我刚入行那会儿,最怕的就是线上程序突然崩了。

日志里就一行 Exception,连个堆栈都看不全。那时候我盯着屏幕,脑子里全是问号——到底哪行代码炸了?数据怎么丢的?

后来我才明白,异常处理不是简单的 try-except 一包了事。它是一门手艺。今天我们就聊聊这门手艺的核心:捕获异常、追踪堆栈、定位问题。

try-except:别把异常吞了

很多新手写代码,喜欢这样:

try:
    result = risky_operation()
except Exception:
    pass

嗯,这代码看着干净,但问题来了——异常被吞了。你永远不知道里面发生了什么。

我个人习惯是:能捕获具体异常,就别用裸的 Exception。比如:

try:
    data = json.loads(raw_string)
except json.JSONDecodeError as e:
    logger.error(f"JSON 解析失败: {e}")
    raise

这样,你知道是 JSON 格式有问题,而不是什么奇奇怪怪的 AttributeError 或 KeyError。

小技巧:捕获异常后,至少打一行日志。别让异常静默消失。线上问题排查时,日志就是你的眼睛。

异常链:别丢了根因

我在项目中遇到过这样一个场景:

一个函数从数据库读取配置,然后解析。如果数据库连接失败,抛 ConnectionError。如果解析失败,抛 ValueError。但有时候,解析失败是因为数据库返回了空数据——这其实是连接问题的连锁反应。

如果只是简单捕获再抛新异常,根因就丢了:

try:
    config = fetch_config()
    parsed = parse_config(config)
except ValueError as e:
    raise RuntimeError("配置解析失败")

你看,fetch_config 里的 ConnectionError 完全消失了。排查时你只会看到 "配置解析失败",然后一脸懵。

正确的做法是用 raise ... from ...:

try:
    config = fetch_config()
    parsed = parse_config(config)
except ValueError as e:
    raise RuntimeError("配置解析失败") from e

这样,异常链就完整了。你看到 RuntimeError 的同时,也能追溯到 ValueError,甚至更底层的 ConnectionError。

注意:不要滥用 raise from None。它会切断异常链,除非你明确知道自己在做什么(比如你想隐藏内部实现细节)。

traceback 模块:自己动手,丰衣足食

有时候,异常被框架捕获了,你拿不到完整的堆栈。或者你想在日志里记录更详细的信息。这时候 traceback 模块就派上用场了。

我常用的几个函数:

函数 作用
traceback.format_exc() 获取当前异常的格式化堆栈字符串
traceback.print_exc() 直接打印堆栈到 stderr
traceback.extract_tb() 提取堆栈的摘要信息(文件名、行号、函数名)
traceback.format_stack() 获取当前调用栈的格式化字符串(即使没有异常)

举个例子,你想在日志里记录完整的异常信息:

import traceback
import logging

logger = logging.getLogger(__name__)

try:
    result = dangerous_call()
except Exception:
    logger.error("发生异常:\n%s", traceback.format_exc())

这样,日志里不仅有异常类型和消息,还有完整的调用栈。排查问题时,一眼就能看到是哪行代码出的问题。

获取精确的调用栈信息

有时候,你需要在没有异常的情况下,知道当前代码是被谁调用的。比如写一个调试工具,或者记录调用链路。

这时候可以用 inspect 模块:

import inspect

def who_called_me():
    frame = inspect.currentframe()
    caller_frame = frame.f_back
    info = inspect.getframeinfo(caller_frame)
    print(f"调用者: {info.filename}:{info.lineno} in {info.function}")

嗯,这招我在写一个性能分析工具时用过。它能精确告诉你每个函数的调用来源,比看日志猜半天靠谱多了。

核心思路:异常处理不是为了掩盖错误,而是为了在错误发生时,留下足够的信息让你快速定位。堆栈就是你的犯罪现场,别破坏它。

知识体系一览

下面这张图,帮你理清今天讲的内容:

异常与堆栈追踪知识体系 异常处理核心 try-except 捕获 捕获具体异常类型 避免吞异常,记录日志 异常链 raise from 保留根因异常 避免 raise from None 滥用 traceback 模块 format_exc / print_exc extract_tb 提取摘要 inspect 模块获取精确调用栈 目标:留下足够信息,快速定位问题

避坑指南

  • 我曾经在代码里写了个大 try-except,把所有异常都捕获了,然后只打印了一个 "Error"。结果线上出了 bug,我花了整整一个下午才找到原因。从那以后,我再也不敢偷懒了。
  • 我曾经用 raise from None 隐藏了一个底层库的异常,结果同事排查时完全摸不着头脑。后来我们约定:除非是安全敏感信息,否则保留异常链。
  • 我曾经在日志里只记录 str(e),没有堆栈。结果异常消息是 "list index out of range",但不知道是哪个列表。现在我的日志模板里,traceback.format_exc() 是标配。

好了,异常处理这块,说白了就是一句话:别让信息丢失。捕获异常时留日志,抛出异常时留根因,排查问题时看堆栈。做到这三点,线上出问题你也能从容应对。

可可精品资料库