{"record":{"id":"03058612b7257df4","repo":"Zackriya-Solutions/meetily","slug":"decode-task-join-error","errorCode":null,"errorMessage":"Decode task join error: {}","messagePattern":"Decode task join error: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"frontend/src-tauri/src/audio/import.rs","lineNumber":388,"sourceCode":"        return Err(anyhow!(\"Import cancelled\"));\n    }\n\n    emit_progress(&app, \"decoding\", 15, \"Decoding audio file...\");\n\n    // Decode the audio file with progress updates\n    let app_for_decode = app.clone();\n    let decode_progress = Box::new(move |progress: u32, msg: &str| {\n        // Map decode progress: 15% + (progress * 0.05) to go from 15% to 20%\n        let overall_progress = 15 + ((progress as f32 * 0.05) as u32);\n        emit_progress(&app_for_decode, \"decoding\", overall_progress, msg);\n    });\n\n    let path_for_decode = dest_path.clone();\n    let decoded = tokio::task::spawn_blocking(move || {\n        decode_audio_file_with_progress(&path_for_decode, Some(decode_progress))\n    })\n    .await\n    .map_err(|e| anyhow!(\"Decode task join error: {}\", e))??;\n    let duration_seconds = decoded.duration_seconds;\n\n    info!(\n        \"Decoded audio: {:.2}s, {}Hz, {} channels\",\n        duration_seconds, decoded.sample_rate, decoded.channels\n    );\n\n    emit_progress(&app, \"resampling\", 20, \"Converting audio format...\");\n\n    // Check for cancellation\n    if IMPORT_CANCELLED.load(Ordering::SeqCst) {\n        let _ = std::fs::remove_dir_all(&meeting_folder);\n        return Err(anyhow!(\"Import cancelled\"));\n    }\n\n    // Convert to 16kHz mono format with progress updates\n    let app_for_resample = app.clone();\n    let resample_progress = Box::new(move |progress: u32, msg: &str| {","sourceCodeStart":370,"sourceCodeEnd":406,"githubUrl":"https://github.com/Zackriya-Solutions/meetily/blob/0281737d87d26352fb0adc78c8c0975f691b23d1/frontend/src-tauri/src/audio/import.rs#L370-L406","documentation":"Raised when `.await` on the `tokio::task::spawn_blocking` handle for `decode_audio_file_with_progress` returns a JoinError, meaning the blocking closure ended abnormally (panicked inside Symphonia, or was cancelled at tokio runtime shutdown) instead of returning Ok/Err. The double `??` in the source distinguishes two failures: the first `?` is this join/panic failure, the second is a normal decode error returned by the decoder. In practice this error means the decoder panicked mid-file.","triggerScenarios":"Importing a file that Symphonia can probe but that makes the decode loop panic (corrupt or truncated stream, container renamed to a misleading extension, unsupported codec path); the decode progress closure panicking while emitting `import-progress`; or the tokio runtime being shut down (app quit) while decoding a large file is still in flight.","commonSituations":"Importing a recording whose writer crashed (truncated MP3/M4A), a partially downloaded audio file, a file renamed from another format to .mp3/.wav, or test/CI runs where the async runtime is dropped before spawn_blocking completes.","solutions":["Get the real panic: run with RUST_BACKTRACE=1 and call `JoinError::try_into_panic()` in the map_err branch, logging or resuming the payload — the backtrace names the failing line in decoder.rs","Verify the file outside the app (`ffprobe file` or `ffmpeg -v error -i file -f null -`) and re-encode it to a clean WAV/MP3 before importing","Fail earlier: run `validate_audio_file` before starting the import pipeline — its metadata fallback already performs a full decode, so bad files surface as a normal decode error before any folders are created","If it only happens at app shutdown, branch on `JoinError::is_cancelled()` and block app exit until imports finish"],"exampleFix":"// before\nlet decoded = tokio::task::spawn_blocking(move || {\n    decode_audio_file_with_progress(&path_for_decode, Some(decode_progress))\n})\n.await\n.map_err(|e| anyhow!(\"Decode task join error: {}\", e))??;\n\n// after — surface the real panic and distinguish cancellation\nlet decoded = match tokio::task::spawn_blocking(move || {\n    decode_audio_file_with_progress(&path_for_decode, Some(decode_progress))\n}).await {\n    Ok(r) => r?,\n    Err(j) if j.is_cancelled() => return Err(anyhow!(\"Decode cancelled (runtime shutdown)\")),\n    Err(j) => {\n        if let Ok(payload) = j.try_into_panic() {\n            error!(\"decode task panicked\");\n            std::panic::resume_unwind(payload); // real backtrace lands in logs\n        }\n        return Err(anyhow!(\"Decode task join error\"));\n    }\n};","handlingStrategy":"try-catch","validationCode":"// pre-flight the decode before starting the import pipeline\nlet info = validate_audio_file(Path::new(&source_path))?; // falls back to full decode on bad metadata\nif info.duration_seconds < 0.1 {\n    return Err(anyhow!(\"Audio too short to decode reliably\"));\n}","typeGuard":null,"tryCatchPattern":"Match the spawn_blocking Result instead of stringifying the JoinError: `Ok(r) => r?, Err(j) if j.is_cancelled() => /* benign at shutdown */, Err(j) => { if let Ok(p) = j.try_into_panic() { log::error!(\"decode panicked\"); std::panic::resume_unwind(p); } /* else surface join error */ }` — never mask the panic payload.","preventionTips":["Run validate_audio_file (or ffprobe) on every file before the import pipeline starts","Keep RUST_BACKTRACE=1 in dev builds so join errors reveal their origin","Re-encode suspicious or renamed files to a canonical format before import","Treat JoinError::is_cancelled() during app shutdown as benign, not an error"],"tags":["tokio","spawn-blocking","join-error","symphonia","audio-decode","panic","rust"],"backgroundTag":"tokio-join-error","analyzedSha":"0281737d87d26352fb0adc78c8c0975f691b23d1","analyzedAt":"2026-08-16T20:57:52.567Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}