{"record":{"id":"f32d706f62cb877b","repo":"gitbutlerapp/gitbutler","slug":"spawn-blocking-join-error-e","errorCode":null,"errorMessage":"spawn_blocking join error: {e}","messagePattern":"spawn_blocking join error: (.+?)","errorType":"exception","errorClass":"napi::Error","httpStatus":null,"severity":"error","filePath":"crates/but-api-macros/src/lib.rs","lineNumber":327,"sourceCode":"        quote! {\n            ::tokio::task::spawn_blocking(move || {\n                #(#napi_param_conversions);*\n                let __napi_body_result: ::anyhow::Result<::serde_json::Value> = (|| {\n                    let result = #napi_call_fn_args?;\n                    #convert_to_json_result_type\n                    Ok(::serde_json::to_value(result)?)\n                })();\n                __napi_body_result.map_err(|e: ::anyhow::Error| {\n                    let ctx = but_error::AnyhowContextExt::custom_context_or_error_chain(&e);\n                    let message = ctx\n                        .message\n                        .map(|m| m.to_string())\n                        .unwrap_or_else(|| format!(\"{e:#}\"));\n                    napi::Error::new(napi::Status::GenericFailure, message)\n                })\n            })\n            .await\n            .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!(\"spawn_blocking join error: {e}\")))?\n        }\n    };\n\n    // For async functions, param conversions happen outside the body (in the async fn).\n    // For sync functions, they're already inside spawn_blocking in napi_body.\n    let napi_external_conversions = if asyncness.is_some() {\n        quote! { #(#napi_param_conversions);* }\n    } else {\n        quote! {}\n    };\n\n    let js_name = fn_name\n        .to_string()\n        .split(\"_\")\n        .enumerate()\n        .map(|(idx, word)| {\n            if idx == 0 {\n                word.into()","sourceCodeStart":309,"sourceCodeEnd":345,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-api-macros/src/lib.rs#L309-L345","documentation":"Generated inside the `but_api` proc-macro's N-API wrapper: the result of `tokio::task::spawn_blocking(...)` is awaited and a `JoinError` (task panicked or was cancelled) is mapped to a napi Error with the literal prefix \"spawn_blocking join error\". This signals the underlying sync API function panicked or the blocking task was aborted, not a normal application error.","triggerScenarios":"Calling any async-wrapped `but_api` N-API function whose spawn_blocking child task panics (unwraps a None/Err, index out of bounds, explicit panic) or is cancelled before completion.","commonSituations":"A bug/regression inside a library function called from JS; the Node process shutting down and cancelling in-flight tasks; unwrap on poisoned locks; stack overflow or assertion failure in the blocking closure.","solutions":["Read the JoinError payload/cancel context and fix the panic in the wrapped Rust function (the join error itself is only a symptom).","Reproduce with the same arguments on the Rust side (unit test) to get the real panic message and backtrace.","Check for process shutdown/cancellation races if the error is JoinError::is_cancelled.","Replace unwrap/expect calls in the API implementation with proper anyhow error propagation."],"exampleFix":"// before\nlet len = ids.len() - 1; // panics when empty\n// after\nlet len = ids.len().checked_sub(1).context(\"empty id list\")?;","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await api.someOperation(args);\n} catch (e) {\n  if (String(e?.message).startsWith('spawn_blocking join error')) {\n    // underlying Rust task panicked or was cancelled; log args and report a bug,\n    // this is not a recoverable domain error\n    console.error('native task crashed', args, e);\n  } else { throw e; }\n}","preventionTips":["Treat this as a bug in the native layer, not bad input; capture args when reporting.","Avoid calling the API during process shutdown where tasks can be cancelled.","Keep the native crate updated; panics behind this wrapper are usually fixed upstream."],"tags":["napi","nodejs","panic","spawn-blocking"],"backgroundTag":"thread-interrupted","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}