本节摘要:批量勘验跑到第 37 个国家突然报错,循环戛然而止——这是自动化时代躲不开的场景。本节讲 R 的三类异常信号(error、warning、message)、tryCatch 护栏写法,以及 traceback、browser、condition 本身的排查动作,让流水线"坏一条不停全线"。
排错的第一课是不要无视 warning——它是 error 的前兆。上一节"空向量倒序循环"那类事故,往往先以 warning 形式露头。
safe_growth <- function(values, years) { tryCatch( growth_rate(values, years), error = function(e) { warning("该国家数据不完整,已跳过:", conditionMessage(e)) NA_real_ # 返回缺失而不是中断 } ) } # 批量跑:个别国家坏了,全线照常收尾 countries <- unique(gapminder_long$country) results <- sapply(countries, function(cty) { d <- gapminder_long[gapminder_long$country == cty, ] safe_growth(d$lifeExp, d$year) }) sum(is.na(results)) # 统计有多少案失败,回头逐个排查
护栏的代价是错误被吞掉。所以失败要记账(warning、日志、NA 标记),事后必须回头清账——只加 tryCatch 不清账,等于把故障埋进结果里。
# 第一件:报错后看调用栈,错误从哪一层抛出 traceback() # 第二件:在函数里插断点,执行到这里进入现场 debug_growth <- function(values, years) { browser() # 停在这里,可查看变量 first <- values[1] last <- values[length(values)] (last / first)^(1 / (years[length(years)] - years[1])) - 1 } # 第三件:RStudio 可视化调试——点行号设断点,与 browser 等效
进入 browser 现场后,常用口令:n 下一行、Q 退出、直接敲变量名查看取值。现场查看的重点永远是输入是否符合预期——九成的函数错误是喂进去的数据形状不对。
⚠️ 常见坑:在循环里裸跑会报错的函数,第 37 次迭代失败时,前 36 次的结果也一并丢失。批量处理前先小样本试跑(比如先跑 5 个国家),确认稳定再放全量。
把本章工具串成一次真实排错。案情:对全部国家批量计算增速,跑到中段抛出 error。完整过程如下:
# 第一步:裸跑复现,读报错原文 sapply(countries, function(cty) { d <- gapminder_long[gapminder_long$country == cty, ] growth_rate2(d$lifeExp, d$year) }) # 报错:values 与 years 长度不一致:12 对 10 # ——某个国家的年份里有重复登记或缺失 # 第二步:用命名向量精确定位是哪个国家 res <- sapply(countries, function(cty) { tryCatch({ d <- gapminder_long[gapminder_long$country == cty, ] growth_rate2(d$lifeExp, d$year) }, error = function(e) NA_real_) }) bad <- names(res)[is.na(res)] # 失败国家名单,一个不漏 # 第三步:进 browser 现场勘验问题国家的原始行 d_bad <- gapminder_long[gapminder_long$country == bad[1], ] d_bad # 肉眼可见重复年份行 d_bad %>% count(year) %>% filter(n > 1) # 机器确认 # 第四步:修复(去重)后重跑,res 全绿
这段复盘展示的节奏值得固化成习惯:复现 → 定位 → 进现场 → 修复 → 全量重跑。第二步"先包 tryCatch 拿到完整失败名单"是关键一跳——比逐个手跑快,也比直接修第一个发现的问题更稳,因为坏数据常常不止一处。
| 工具 | 用在什么阶段 | 一句话定位 |
|---|---|---|
| traceback | 报错之后立即看 | 回答"错在哪一层" |
| browser 断点 | 知道哪段代码可疑 | 回答"运行到这行时变量什么样" |
| recover 模式 | 报错现场想逐层挑 | options(error = recover) 后报错进选单 |
| tryCatch | 修完之后上量产 | 回答"个别失败如何不影响全线" |
| 小样本试跑 | 放全量之前 | 用 5% 的成本暴露 95% 的问题 |
时序上它们不是并列选项而是流水线:前四件围着一 次故障转,第五件是量产前的例行安检。把顺序用反(比如一上来就 tryCatch 把错误吞掉再瞎猜)是排错中最常见的弯路。
排错的终点站要回答一个判断题:这次故障的病根在数据还是在代码?判据有两条——病发范围(只有个别国家出错,多半是数据;全部一致出错,多半是代码);复现稳定性(同一输入稳定复现,代码问题概率大;换批数据就好了,数据问题概率大)。数据病修数据(去重、补缺、口径统一),代码病修代码(校验、边界、类型),修错了方向会陷入"补丁摞补丁"的泥潭。