{"record":{"id":"e95b43e076fb2d8b","repo":"gitbutlerapp/gitbutler","slug":"genericfailure","errorCode":"GenericFailure","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":303,"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":285,"sourceCodeEnd":321,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-api-macros/src/lib.rs#L285-L321","documentation":"Generated by the but-api-macros N-API wrapper: the Rust function body runs inside tokio's spawn_blocking, and the JoinError returned when awaiting it is converted into a napi GenericFailure with 'spawn_blocking join error'. A JoinError practically always means the closure panicked (or the runtime shut it down), so this message indicates a Rust-side panic crossing the JS boundary, not a normal API error.","triggerScenarios":"Calling any #[but_api]-exported function from JS whose Rust implementation panics (unwrap on None, index out of bounds, assertion failure); shutting down the tokio runtime while a blocking API call is still in flight.","commonSituations":"A panic path in library code reachable only for unusual repository states (missing ref, corrupt index); a regression introduced by a refactoring that added an unwrap; calling the desktop app's N-API layer during app teardown.","solutions":["Reproduce with the same arguments and inspect the Rust backtrace (RUST_BACKTRACE=1 in the desktop app logs) to find the actual panic site inside the function body.","Fix the panic at its source (replace unwrap/expect with proper error propagation) — the join-error text itself is only a symptom.","If it happens only at shutdown, ensure API calls complete before tearing down the runtime.","If the panic is inside a GitButler crate you do not own, report it with the backtrace and the repository state that triggered it."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await api.someFn(args);\n} catch (e) {\n  if (String(e?.message).includes('spawn_blocking join error')) {\n    // Rust-side panic: capture args + app logs and report; not retryable as-is\n    reportPanic('api.someFn', args, e);\n  }\n  throw e;\n}","preventionTips":["Treat 'spawn_blocking join error' as a Rust panic: always attach the desktop app logs when reporting.","Keep a reproducible argument set; panics are input- or state-dependent.","Library authors: prefer Result propagation over unwrap/expect in code reachable through #[but_api] exports."],"tags":["rust","napi","tokio","panic","async"],"backgroundTag":"worker-thread-panic","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}