Zackriya-Solutions/meetily · error · anyhow::Error

Decode task panicked

Error message

Decode task panicked: {}

What it means

decode_audio_file ran inside tokio::task::spawn_blocking and the task ended in a panic; the JoinError is reported as 'Decode task panicked'. The decoder crashed on its input (an unwrap/index/alloc failure inside the decoding path) rather than returning an Err, so the audio file itself is the prime suspect.

Solutions

  1. Open the file in another player to verify integrity; re-export or discard damaged audio
  2. Log the panic payload (join_error.into_panic() downcast to String/&str) to locate the decoder crash site
  3. Pre-validate the file (header sanity, size vs. declared length) before decode, or wrap decode_audio_file in catch_unwind to convert panics into errors

Example fix

// before
let decoded = tokio::task::spawn_blocking(move || decode_audio_file(&path))
    .await
    .map_err(|e| anyhow!("Decode task panicked: {}", e))??;

// after - convert the panic into an error carrying the payload
let decoded = tokio::task::spawn_blocking(move || {
    std::panic::catch_unwind(|| decode_audio_file(&path))
        .map_err(|p| anyhow!("decode panicked: {:?}", p.downcast_ref::<String>().cloned()))
})
.await
.map_err(|e| anyhow!("Decode task panicked: {}", e))???;
Defensive patterns

Strategy: try-catch

Validate before calling

// Cheap pre-decode sanity: header present and size plausible
fn audio_plausibly_valid(path: &std::path::Path) -> bool {
    let mut buf = [0u8; 12];
    match std::fs::File::open(path) {
        Ok(mut f) => std::io::Read::read_exact(&mut f, &mut buf).is_ok() && buf.len() == 12,
        Err(_) => false,
    }
}

Try / catch

Treat the JoinError distinctly from decode errors: inspect .is_panic() and downcast into_panic() for the message; report 'audio file is corrupt or unsupported' to the user and offer file repair/replacement rather than retry (retrying a panic on the same input will panic again).

Prevention

When it happens

Trigger: Corrupted or truncated audio file - e.g., the app was killed mid-save leaving a WAV/M4A header claiming more data than the file holds; a container/codec edge case that trips an unwrap in the decoder; allocation failure on a pathologically large file.

Common situations: Retranscribing a recording whose save was interrupted by a crash or force-quit; partially copied or synced audio files; exotic files produced by nonstandard recorders.

Related errors


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

Appendix: source

Thrown at frontend/src-tauri/src/audio/retranscription.rs:206

        "Starting retranscription for meeting {} with language {:?}, model {:?}, provider {:?}",
        meeting_id, language, model, provider
    );

    // Emit progress: decoding
    emit_progress(&app, &meeting_id, "decoding", 5, "Decoding audio file...");

    // Check for cancellation
    if RETRANSCRIPTION_CANCELLED.load(Ordering::SeqCst) {
        return Err(anyhow!("Retranscription cancelled"));
    }

    // Decode the audio file (CPU-intensive, run in blocking task)
    let path_for_decode = audio_path.clone();
    let decoded = tokio::task::spawn_blocking(move || {
        decode_audio_file(&path_for_decode)
    })
    .await
    .map_err(|e| anyhow!("Decode task panicked: {}", e))??;
    let duration_seconds = decoded.duration_seconds;

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

    emit_progress(&app, &meeting_id, "decoding", 15, "Converting audio format...");

    // Check for cancellation
    if RETRANSCRIPTION_CANCELLED.load(Ordering::SeqCst) {
        return Err(anyhow!("Retranscription cancelled"));
    }

    // Convert to 16kHz mono format (CPU-intensive, run in blocking task)
    let audio_samples = tokio::task::spawn_blocking(move || {
        decoded.to_whisper_format()
    })

View on GitHub (pinned to a2cb62e827)