
1. 库 throw、节点 catch 不到——策略混用的代价
driver decode 热路径 throw runtime_error,rclcpp callback 未 catch——偶发 process 退出。另一头算法库只 RCLCPP_WARN 不 metric,field 统计不到 parse 失败率。根因是 模块边界没有统一的失败契约:库内、节点边缘、ROS service 边界应各用一种策略,别 triple(bool + errno + throw)。新同事最容易在「库裡 throw 方便」与「节点裡 expected 统一」之间混用,集成后 executor 间歇死亡。
库内「抛异常」与产品边缘「记日志 + 降级」是两种策略——混用会导致 driver 层 catch 不到、或 UI 线程被 exception 打断。机器人 stack 倾向:库用 expected/错误码,节点边界统一转 RCLCPP_ERROR + 安全态。错误策略写进模块 README 与 ADR,比 code review 口头提醒可靠。
2. 库层:可失败是类型的一部分
#include <expected>
std::expected<Pose, ParseError> parse_odometry(std::span<const uint8_t> frame) {
if (frame.size() < kMinSize)
return std::unexpected(ParseError::TooShort);
return Pose{ /* ... */ };
}C++23 std::expected;C++17 可用 tl::expected 或 absl::StatusOr。decode 热路径 avoid throw——部分 RT 环境 exception 成本高。跨 .so 用 enum class Error : uint32_t 稳定 ABI;字符串用固定 buffer 或 caller 分配。expected 链式 and_then 时错误类型要可合并,别每层不同 exception 导致 node 边缘 catch 爆炸。
3. 节点边界:查返回值,catch 兜底
void Node::on_packet(const Packet& p) {
try {
auto pose = parse_odometry(p.data);
if (!pose) {
RCLCPP_WARN_THROTTLE(get_logger(), *clock_, 1000,
"parse fail: %d", static_cast<int>(pose.error()));
++parse_errors_;
return;
}
publish(*pose);
} catch (const std::exception& e) {
RCLCPP_ERROR(get_logger(), "cb: %s", e.what());
}
}Lifecycle inactive 时不 publish——错误处理与状态机联动。ROS 2 service 返回 success=false,别 throw 穿越 rclcpp 边界。最外层 catch ... 后 rclcpp::shutdown,别让异常冒过 main。service callback 返回 false 或带 success=false 的 result message,别 throw 穿越 rclcpp 边界。
4. noexcept 与 move
move/swap 标 noexcept 时内部若 throw 会 terminate。库边界 move 只挪指针可 noexcept;解析 YAML 的 move 不要 noexcept。.so 里 catch ... 转 ErrorCode::Unknown 返回;主程序 catch 后继续 publish 未初始化 pose 是致命 bug。plugin 返回稳定 error enum,别用 magic int。move/swap 标 noexcept 时,内部若 throw expected 失败会 terminate。
5. 与运维 playbook 对齐
error_code 与 oncall playbook 编号一致——log、metrics、文档同名。单测 mock corrupt packet fuzz,断言节点不 crash、parse_errors 递增。同一函数既返回 bool 又设 errno 又 throw——call site 必漏;团队定一条:核心 C++ 库 expected,Python binding 再转 exception。expected 错误码与运维 playbook 编号一致,oncall 可按码检索。
6. 案例:未捕获 exception 杀 executor
某 laser 节点 decoder 抛 std::out_of_range,callback 无 catch——进程间歇退出,bag 里只有「node died」。最外层加 try/catch 映射 ERROR + metric 后, corrupt packet 只 increment counter 不杀进程。库层仍改 expected,throw 仅作 legacy 兜底。错误被吞——只 log WARN 不 metric,field 无法统计失败率。说明边界策略必须是团队契约,而不是个人习惯。
7. 库内 expected,边缘 catch
算法库 API 返回 tl::expected<Pose, ErrorCode>;ROS node 边缘把 ErrorCode 映射成 RCLCPP_ERROR + lifecycle deactivate。库内 throw runtime_error 穿过 callback 会让 rclcpp executor 状态不确定。plugin 返回稳定 error enum;noexcept swap 保证 vector strong safety。
8. 安全态与 metric
parse 连续失败须计数并触发 degrade(降频、停 publish、lifecycle deactivate),别 infinite WARN。metric parse_errors_total{code} 与 log error_code 同源。安全相关错误(collision、estop)走 ERROR + 硬动作,与可恢复 decode 错误分级。oncall playbook 按 code 写第一步动作,不是「看日志猜」。
9. Python binding 与插件边界
pybind 层可把 expected 转 exception,但 C++ 核心库保持 expected——边界清晰。plugin .so export C ABI 时 error enum 稳定,字符串 buffer 由 caller 分配。同一错误别在 Python 抛、在 C++ return、在 ROS 又 log 三种格式,fleet 无法聚合。vendor SDK 若只能 throw,在 driver 边界 catch 一次转 error code,别让 throw 穿过 rclcpp callback。
10. 降级策略与 lifecycle
连续 parse 失败应触发 degrade:停 publish、降频、或 lifecycle deactivate——仅 WARN 等于沉默失败。ERROR 与「可恢复 WARN」分级写进 playbook。fuzz 与 fault injection 进 CI:corrupt packet 不杀 process,计数器递增,behavior 可预测。
10. 验收
- 模块 README 写清:调用方该查返回值还是该 catch。
- 无未捕获 exception 导致 executor 线程退出。
- 错误不吞:WARN 以上有 metric 或计数器。
- plugin/service 边界无 throw 穿越;expected 错误码稳定。
- 注入 corrupt packet fuzz,断言节点不 crash。
12. 与 service/action 边界
service callback 返回 success=false 比 throw 更可控;action 取消路径也须 expected 风格,cancel 时 resource 释放 noexcept。long-running service 内部分步 error 写进 result message 的 code 字段,client 按 code 重试或 abort——别只返回空 failure。vendor driver 若只能 throw,在 driver 模块边界 catch 一次转 error enum,禁止 exception 穿透到 rclcpp executor 线程。Python binding 在边界转 exception 可以,但 C++ 核心库与 node 之间仍 expected——三层别混两种策略。fuzz corrupt packet 进 CI nightly,assert 进程存活且 metric 递增。模块边界 README 一页纸写清 expected/catch 分工,新人 onboarding 必读——比 scattered wiki 可靠。
事故 bag 里若有 parse_errors metric 或 ERROR code 时间线,能分清 corrupt 数据 vs 算法退化。无 metric 的 WARN 风暴只剩人肉 grep。错误边界策略应进 onboarding:新 driver 接入 checklist 含 expected 映射与 fuzz case。Lifecycle inactive 时停止 publish 与错误处理联动——parse 失败仍 publish 旧 pose 是安全漏洞。
相关
也可以看看
johan's blog