如何捕获 JS 异常?
解读
在国内前端与 Rust 混合开发的面试场景里,面试官问“如何捕获 JS 异常”并不是想听一段纯前端代码,而是考察候选人能否在 Rust 侧安全、零成本地接管 JavaScript 运行时错误,并给出可维护、可扩展、可测试的解决方案。核心诉求有三点:
- 不 panic:Rust 线程绝不能因为 JS 抛错而崩溃;
- 不丢信息:堆栈、行列号、SourceMap 都要完整回传 Rust;
- 不阻塞性能:异常路径不能引入额外 GC 压力或锁竞争。
知识点
- wasm-bindgen 异常 ABI:js_sys::Error、wasm_bindgen::JsValue、JsCast trait
- Closure::wrap 与 catch_unwind 边界:FFI 安全栅栏,防止 Rust panic 穿透到 JS
- std::panic::AssertUnwindSafe 与 Send+Sync 约束:在异步或并发场景下保证 unwind 安全
- source-map-mappings crate:在 Rust 侧解析 .map 文件,实现行列号回译
- Window::set_error_handler 与 queueMicrotask:捕获异步宏任务错误
- #[wasm_bindgen(start)] 初始化钩子:注册全局错误监听器,做到“编译通过即正确”
答案
分三层回答,先给最小可运行示例,再讲原理,最后给出工程化封装。
- 最小可运行示例
use wasm_bindgen::prelude::*;
use web_sys::{ErrorEvent, Window};
#[wasm_bindgen(start)]
pub fn init() -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
let closure = Closure::wrap(Box::new(move |e: ErrorEvent| {
let msg = e.message();
let file = e.filename();
let line = e.lineno();
let col = e.colno();
// 发送到 Rust 日志系统
web_sys::console::error_1(&format!(
"JS异常: {} at {}:{}:{}", msg, file, line, col
).into());
// 阻止继续冒泡,避免控制台重复打印
e.prevent_default();
}) as Box<dyn FnMut(ErrorEvent)>);
window.set_onerror(Some(closure.as_ref().unchecked_ref()));
closure.forget(); // 全局长生命周期,不 drop
Ok(())
}
编译后,任何 <script> 或 wasm-bindgen 生成的胶水代码抛错都会被 Rust 闭包捕获。
- 原理拆解
- set_onerror 只能捕获同步错误;对于 Promise 未处理拒绝,需要再注册
window.addEventListener("unhandledrejection", …)。 - 闭包内部使用
web_sys::console::error_1只是示例,生产环境应通过js_sys::Reflect::set把错误对象序列化到 Rust 结构体,再送入日志通道(如tokio::sync::mpsc或tracing)。 - closure.forget() 把所有权交给 JS GC,Rust 侧不再管理,因此必须保证闭包内部无 panic;否则应使用
std::panic::catch_unwind做二次防护。
- 工程化封装
#[wasm_bindgen]
pub struct JsErrorCapture {
tx: std::sync::mpsc::Sender<JsValue>,
}
#[wasm_bindgen]
impl JsErrorCapture {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
let (tx, rx) = std::sync::mpsc::channel();
let window = web_sys::window().unwrap();
// 同步错误
let tx1 = tx.clone();
let onerror = Closure::wrap(Box::new(move |e: ErrorEvent| {
let _ = tx1.send(e.to_js_value());
}) as Box<dyn FnMut(ErrorEvent)>);
window.set_onerror(Some(onerror.as_ref().unchecked_ref()));
onerror.forget();
// 异步拒绝
let tx2 = tx.clone();
let onunhandled = Closure::wrap(Box::new(move |e: web_sys::PromiseRejectionEvent| {
let _ = tx2.send(e.reason());
}) as Box<dyn FnMut(web_sys::PromiseRejectionEvent)>);
window.add_event_listener_with_callback(
"unhandledrejection",
onunhandled.as_ref().unchecked_ref(),
).unwrap();
onunhandled.forget();
// 启动 Rust 侧日志任务
wasm_bindgen_futures::spawn_local(async move {
while let Ok(err) = rx.recv() {
if let Ok(err) = err.dyn_into::<js_sys::Error>() {
let stack = err.stack().as_string().unwrap_or_default();
// 这里可以把 stack 发给 Sentry、阿里云 SLS 或自研日志平台
log::error!("JS异常堆栈\n{}", stack);
}
}
});
JsErrorCapture { tx }
}
}
该封装满足:
- 零成本抽象:release 模式下闭包直接生成 JS 函数指针,无额外内存分配;
- 内存安全:所有 JS 值通过
JsValue强类型穿越 FFI,Rust 侧不手动 free; - 并发无数据竞争:通道使用
std::sync::mpsc,与 JS 单线程事件循环天然匹配。
拓展思考
- SourceMap 回译:生产环境代码经过压缩,行列号失真。可在 Rust 侧引入
source-map-mappingscrate,在编译期把.map文件嵌入二进制,异常到达后即时解析,还原原始 TS/Rust 源码位置,方便国内云原生监控平台(如阿里云 ARMS、腾讯云 TSW)直接展示。 - 异步错误链路追踪:结合
wasm-bindgen-futures的future_to_promise,在 Rust async 函数内部使用tracing::span!记录上下文,一旦 Promise 被拒绝,可把完整的 Rust async 栈与 JS 栈拼接,实现“跨语言链路追踪”。 - 与
std::panic::catch_unwind联动:在 Rust 侧故意触发 panic 时,用catch_unwind捕获后封装成js_sys::Error,再抛回 JS,形成“双向异常”通道,确保无论哪一侧先出错,都能统一由 Rust 日志系统收口,满足金融级 Rust wasm 微服务对“可观测性”的合规要求。