Zackriya-Solutions/meetily · error

Decode task join error: {}

Error message

Decode task join error: {}

What it means

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.

Source

Thrown at frontend/src-tauri/src/audio/import.rs:388

        return Err(anyhow!("Import cancelled"));
    }

    emit_progress(&app, "decoding", 15, "Decoding audio file...");

    // Decode the audio file with progress updates
    let app_for_decode = app.clone();
    let decode_progress = Box::new(move |progress: u32, msg: &str| {
        // Map decode progress: 15% + (progress * 0.05) to go from 15% to 20%
        let overall_progress = 15 + ((progress as f32 * 0.05) as u32);
        emit_progress(&app_for_decode, "decoding", overall_progress, msg);
    });

    let path_for_decode = dest_path.clone();
    let decoded = tokio::task::spawn_blocking(move || {
        decode_audio_file_with_progress(&path_for_decode, Some(decode_progress))
    })
    .await
    .map_err(|e| anyhow!("Decode task join error: {}", e))??;
    let duration_seconds = decoded.duration_seconds;

    info!(
        "Decoded audio: {:.2}s, {}Hz, {} channels",
        duration_seconds, decoded.sample_rate, decoded.channels
    );

    emit_progress(&app, "resampling", 20, "Converting audio format...");

    // Check for cancellation
    if IMPORT_CANCELLED.load(Ordering::SeqCst) {
        let _ = std::fs::remove_dir_all(&meeting_folder);
        return Err(anyhow!("Import cancelled"));
    }

    // Convert to 16kHz mono format with progress updates
    let app_for_resample = app.clone();
    let resample_progress = Box::new(move |progress: u32, msg: &str| {

View on GitHub (pinned to 0281737d87)

Solutions

  1. 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
  2. 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
  3. 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
  4. If it only happens at app shutdown, branch on `JoinError::is_cancelled()` and block app exit until imports finish

Example fix

// before
let decoded = tokio::task::spawn_blocking(move || {
    decode_audio_file_with_progress(&path_for_decode, Some(decode_progress))
})
.await
.map_err(|e| anyhow!("Decode task join error: {}", e))??;

// after — surface the real panic and distinguish cancellation
let decoded = match tokio::task::spawn_blocking(move || {
    decode_audio_file_with_progress(&path_for_decode, Some(decode_progress))
}).await {
    Ok(r) => r?,
    Err(j) if j.is_cancelled() => return Err(anyhow!("Decode cancelled (runtime shutdown)")),
    Err(j) => {
        if let Ok(payload) = j.try_into_panic() {
            error!("decode task panicked");
            std::panic::resume_unwind(payload); // real backtrace lands in logs
        }
        return Err(anyhow!("Decode task join error"));
    }
};
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-flight the decode before starting the import pipeline
let info = validate_audio_file(Path::new(&source_path))?; // falls back to full decode on bad metadata
if info.duration_seconds < 0.1 {
    return Err(anyhow!("Audio too short to decode reliably"));
}

Try / catch

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.

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Zackriya-Solutions/meetily@0281737d87 (2026-08-16). Data as JSON: /api/errors/03058612b7257df4. Report an issue: GitHub.