{"record":{"id":"d91b03646af817e9","repo":"gitbutlerapp/gitbutler","slug":"update-check-thread-panicked","errorCode":null,"errorMessage":"Update check thread panicked","messagePattern":"Update check thread panicked","errorType":"panic","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/but-update/src/check.rs","lineNumber":138,"sourceCode":"            let response = client\n                .post(url)\n                .json(&request_body)\n                .send()\n                .await\n                .map_err(|e| anyhow::anyhow!(\"Request failed: {e}\"))?\n                .error_for_status()\n                .map_err(|e| anyhow::anyhow!(\"Server returned error: {e}\"))?;\n\n            let update_info = response\n                .json::<CheckUpdateStatus>()\n                .await\n                .map_err(|e| anyhow::anyhow!(\"Failed to parse response: {e}\"))?;\n\n            Ok(update_info)\n        })\n    })\n    .join()\n    .map_err(|_| anyhow::anyhow!(\"Update check thread panicked\"))?;\n\n    // Save to cache (convert to but_db types)\n    if let Ok(status) = &result {\n        let now = chrono::Utc::now();\n\n        // Get existing cache to preserve suppression\n        let existing = trans.update_check().get();\n        let (suppressed_at, suppress_duration_hours) = existing\n            .and_then(|cached| cached.suppressed_at.zip(cached.suppress_duration_hours))\n            .and_then(|(suppressed_at, duration_hours)| {\n                let suppress_until = suppressed_at + chrono::Duration::hours(duration_hours as i64);\n                if now > suppress_until {\n                    None\n                } else {\n                    Some((suppressed_at, duration_hours))\n                }\n            })\n            .unzip();","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-update/src/check.rs#L120-L156","documentation":"The dedicated thread spawned for the update check panicked (or was forcibly terminated), so join() returned Err and the payload was discarded. The real panic message went to stderr; this error only says the join failed. Inside the thread, panics can come from the runtime build, the request, or response handling.","triggerScenarios":"Any panic inside the spawned closure — e.g. a reqwest/tokio invariant, an unwrap in the caching code after join, or the thread aborted due to stack overflow/OOM kill.","commonSituations":"Rare; typically surfaces with a concurrent stderr panic backtrace. Aborts from OOM also manifest this way.","solutions":["Check stderr/logs for the actual panic backtrace that accompanied this error","Update check is non-critical: catch this error and continue with cached/no update info","If reproducible, run under a debugger or RUST_BACKTRACE=full to capture the panicking frame","Upgrade but-update — panics in this path get fixed like any bug"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"if let Err(e) = check_status(...) {\n    if e.to_string().contains(\"thread panicked\") {\n        // capture stderr/backtrace in telemetry; fall back to cached status\n    }\n}","preventionTips":["Always log the panic backtrace accompanying this join error — the payload here is lossy","Make update checks fail-open (cached result, not an error dialog)","Upgrade promptly; panics in this path are ordinary bugs that get patched"],"tags":["rust","thread","panic","update-checker","but-update"],"backgroundTag":"worker-thread-panic","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}