failed to initiate panic, error 5, aborting
主线位置:调用主线的「同步命令就地执行」一步。同步命令是 wry 的
start_task(WebKit 回调WKURLSchemeHandler的入口)一路同步调下来的,它的 panic 要往上穿过这些栈帧。
flowchart TB wk["WebKit 栈帧"] --> st["wry start_task"] st --> proto["ipc 协议处理"] proto --> onmsg["on_message"] onmsg --> cmd["同步命令 panic"] cmd -->|"向上找 catch"| fail["error 5 找不到"] fail --> abort["abort 退出码 134"]
本页的崩溃输出都是之前几页的实验留下的(app-life,tauri 2.12.5,debug 构建),没有为本页重新跑会崩溃的程序:在 macOS 上,进程 abort 会弹出系统的崩溃报告对话框。修法部分是新跑的,用的是实验 crate pit2-app(MODE=panic,debug 构建),不会崩溃。平台 M2 Max、macOS 27、rustc 1.99.0。
现象
app-life 的 report 命令是同步命令,设 RT=late 时在里面调 async_runtime::set。这时运行时早已初始化,set panic(这件事本身见异步运行时):
#[tauri::command]
fn report(app: tauri::AppHandle, msg: String) {
println!("REPORT\n{msg}");
if std::env::var("RT").as_deref() == Ok("late") {
log("calling async_runtime::set after the page loaded");
let rt = tokio::runtime::Runtime::new().unwrap();
tauri::async_runtime::set(rt.handle().clone());
}
// ...
}
853ms [main] calling async_runtime::set after the page loaded
thread 'main' (2368571) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.12.5/src/async_runtime.rs:282:25:
runtime already initialized
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
fatal runtime error: failed to initiate panic, error 5, aborting
exit=134
panic 消息照常打印,接着是一行 fatal runtime error,进程退出码 134(128 + 6,SIGABRT),不是普通 panic 的 101。report 里后面的 app.exit(0) 没执行,RunEvent::ExitRequested / Exit 都没有发出,所有 Drop 都没机会跑:缓冲区里没刷到磁盘的数据丢了,临时文件、锁文件原样留下。
同一份程序换一种 panic 也一样。RT=tokiomain 模拟 #[tokio::main]:主线程在 Runtime::block_on 里,同步命令 block_sync 再 block_on,tokio panic「Cannot start a runtime from within a runtime」:
564ms [main] block_sync: async_runtime::block_on on this thread
thread 'main' (2470140) panicked at src/main.rs:87:3:
Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks. If you are in an async function, use `.await` on the future instead of blocking on it.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
fatal runtime error: failed to initiate panic, error 5, aborting
exit=134
两次 panic 的来源完全不同,结局一样。下一节会看到,决定结局的是 panic 发生的位置,不是它的内容;unwrap() 一个 None、数组越界在同一个位置上也是这样(这两种没有为本页单独跑)。
出处
加 RUST_BACKTRACE=1 跑第一个例子,回溯是这样的(app-life/bt-late.txt;省略的第 8、12、14 帧是很长的 Box<dyn Fn ...> 调用):
thread 'main' (2471366) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.12.5/src/async_runtime.rs:282:25:
runtime already initialized
stack backtrace:
0: __rustc::rust_begin_unwind
1: core::panicking::panic_fmt
2: tauri::async_runtime::set::{closure#0}
3: <core::result::Result<(), tauri::async_runtime::GlobalRuntime>>::unwrap_or_else::<tauri::async_runtime::set::{closure#0}>
4: tauri::async_runtime::set
5: app_life::report
6: app_life::start::{closure#3}::{closure#0}
7: app_life::start::{closure#3}
...
9: <tauri::manager::AppManager>::run_invoke_handler
10: <tauri::webview::Webview>::on_message
11: tauri::ipc::protocol::get::<tauri_runtime_wry::Wry<tauri::EventLoopMessage>>::{closure#0}
...
13: <tauri::manager::webview::WebviewManager<tauri_runtime_wry::Wry<tauri::EventLoopMessage>>>::prepare_pending_webview::<tauri::app::AppHandle>::{closure#2}
...
15: tauri_runtime_wry::create_webview::<tauri::EventLoopMessage>::{closure#8}
16: wry::wkwebview::class::url_scheme_handler::start_task
17: <unknown>
18: <unknown>
19: <unknown>
20: <unknown>
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
fatal runtime error: failed to initiate panic, error 5, aborting
从命令往上数:run_invoke_handler、on_message、ipc 协议的闭包、prepare_pending_webview 包的一层、tauri-runtime-wry 包的一层,然后是 wry 的 start_task(wry 0.57.1),再往上全是 <unknown>,即 WebKit 和系统框架的 Objective-C 栈帧。start_task 是注册给 Objective-C 运行时的方法实现,声明为 extern "C" fn:
// Task handler for custom protocol
extern "C" fn start_task(
this: &AnyObject,
_sel: objc2::runtime::Sel,
webview: &WryWebView,
task: &ProtocolObject<dyn WKURLSchemeTask>,
) {
Rust 的 panic 分两步:先打印消息(panic hook),再调平台的 _Unwind_RaiseException 开始展开。展开的第一阶段只「找」:从 panic 处往上逐帧看谁能接住。整条链上,从命令到 start_task 没有一处 catch_unwind(tauri 2.12.5 整个仓库只有 tauri-macros 的移动端入口宏用到它,wry 0.57.1 里没有),再往上是 WebKit 的帧。第一阶段没有找到接住它的帧,_Unwind_RaiseException 返回错误码 5,标准库的 unwind 里这个值叫 _URC_END_OF_STACK(「到栈底了」);std 的 rust_panic 拿到返回值后打印 failed to initiate panic, error {code} 并 abort。这就是为什么一个 Drop 也没执行:第二阶段(真正的展开)根本没开始。
对照两种不会 abort 的 panic:
- 在 async 命令里:命令体是
respond_async_serialized交给async_runtime的任务,tokio 在 poll 任务时自己包了catch_unwind,panic 只结束这个任务。下文的修法里有实测。 - 在 tao 事件循环的回调里:tao 0.37.2 在 macOS 上把 run loop observer 驱动的回调包在
stop_app_on_panic里,接住 panic、停掉NSApplication,等app.run()返回后再resume_unwind。这条只读了源码,本卷没有为它跑会 panic 的程序。
ipc:// 请求从 WebKit 直接进 start_task,不经过 tao 的这一层。同样的结局也落在 tauri 自己的代码上:2.12.4 之前,ipc/protocol.rs 取命令名时对空路径做切片会越界,一个 ipc:foo 这样的请求就能让进程 abort(#16197),见 ipc 协议。
Windows 和 Linux 上协议回调的调用栈不同,结果是否一样本卷没有实测。
修法
pit2-app 里三个版本的「取第 i 个元素」,都用越界的下标调用:
#[tauri::command]
async fn nth_async(items: Vec<u32>, i: usize) -> u32 {
items[i]
}
#[tauri::command]
fn nth_checked(items: Vec<u32>, i: usize) -> Result<u32, String> {
items
.get(i)
.copied()
.ok_or_else(|| format!("index {i} out of range for {} items", items.len()))
}
#[tauri::command]
fn nth_caught(items: Vec<u32>, i: usize) -> Result<u32, String> {
std::panic::catch_unwind(|| items[i]).map_err(|e| {
let msg = e
.downcast_ref::<String>()
.cloned()
.or_else(|| e.downcast_ref::<&str>().map(|s| s.to_string()))
.unwrap_or_default();
format!("command panicked: {msg}")
})
}
页面依次调用,nth_async 套一个 1 秒的超时,最后再调一次 greet 看应用还在不在:
out.nthChecked = await attempt(() => inv('nth_checked', { items: [1, 2, 3], i: 7 }));
out.nthCaught = await attempt(() => inv('nth_caught', { items: [1, 2, 3], i: 7 }));
out.nthAsync = await attempt(() => within(inv('nth_async', { items: [1, 2, 3], i: 7 }), 1000));
out.greetAfter = await attempt(() => inv('greet', { name: 'still alive' }));
0ms [main] main
83ms [main] probe plugin setup
thread 'main' (2735288) panicked at src/main.rs:105:36:
index out of bounds: the len is 3 but the index is 7
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'tokio-rt-worker' (2735425) panicked at src/main.rs:92:8:
index out of bounds: the len is 3 but the index is 7
REPORT {
"mode": "panic",
"nthChecked": {
"err": "index 7 out of range for 3 items"
},
"nthCaught": {
"err": "command panicked: index out of bounds: the len is 3 but the index is 7"
},
"nthAsync": {
"err": "no answer after 1000 ms"
},
"greetAfter": {
"ok": "hello, still alive"
}
}
1537ms [main] ExitRequested { code: Some(0) }
1537ms [main] Exit
exit=0
nth_checked:不 panic,把错误当返回值。JS 的 Promise 以这个字符串拒绝。这是首选,错误类型怎么序列化见参数、返回值与 InvokeResolver。nth_caught:panic 发生在主线程上(thread 'main' ... src/main.rs:105,消息照样打印),但这次第一阶段在命令自己的catch_unwind里就找到了接住它的地方,展开正常进行,命令返回Err,进程继续。适合包住你控制不了的代码(第三方库、复杂的解析)。nth_async:panic 发生在tokio-rt-worker上,进程没死,但这次调用永远没有应答:tokio 接住 panic 后结束了任务,InvokeResolver的应答闭包没被调用,JS 只能靠自己的超时。所以「改成 async」只是保住了进程,async 命令里同样该返回Result。- 最后的
greet正常返回,退出码 0。
Cargo.toml 里设 panic = "abort" 帮不上忙:panic 直接 abort,少的只是「找不到」这一步,而且上面的 catch_unwind 也随之失效。想让任何 panic 都不至于静默失败,可以用 std::panic::set_hook 在 abort 之前把消息写进日志,hook 在 abort 之前执行(上面的输出里 panic 消息都打印出来了)。
怎么确认没中招
- 退出码 134、终端最后一行是
fatal runtime error: failed to initiate panic, error 5, aborting、macOS 弹出崩溃报告:基本就是主线程上的同步代码 panic 了。往上找thread 'main' ... panicked at那一行看位置。 - 搜同步命令(没有
async的#[tauri::command])里的unwrap()、expect(、[i]下标、panic!、unreachable!,以及会 panic 的库调用(async_runtime::set、block_on)。 - async 命令里的 panic 不会 abort,但会让 Promise 永远挂着;在前端给关键调用加超时,在 Rust 侧用
set_hook记日志,才能发现它。
小结
- macOS 上同步命令在 wry 的
start_task之下执行,往上是 WebKit 的栈帧;panic 找不到接住它的地方,_Unwind_RaiseException返回 5,进程 abort,退出码 134,不执行任何Drop。 - async 命令里的 panic 被 tokio 接住,进程活着,但这次调用永远不应答。
- 修法:命令返回
Result,不用 panic 表达错误;包不住的代码用catch_unwind在命令内部接住;async 命令同样返回Result。