loco-rs/loco · error

db cleanup thread panicked

Error message

db cleanup thread panicked

What it means

After running the async cleanup on a dedicated thread, `cleanup_db` calls `.join()` on the thread handle. If the thread panicked for any reason (any of the inner expects: runtime creation, connect, drop), the join returns Err and this final `.expect("db cleanup thread panicked")` re-panics on the caller, converting a teardown failure into a test-suite failure.

Solutions

  1. Fix the root cause reported in the thread panic output (see the earlier 'caused by' message printed before this one)
  2. Make cleanup resilient: catch join errors and log a warning instead of panicking, so flaky DB teardown doesn't fail otherwise-green tests
  3. Run teardown inside the test's own async runtime when possible to avoid the thread+runtime hop entirely
  4. Ensure Postgres availability and clean connection state before teardown (see prior solutions)

Example fix

// before
.join()
.expect("db cleanup thread panicked");
// after
if let Err(e) = handle.join() {
    eprintln!("warning: test db cleanup failed: {e:?}");
}
Defensive patterns

Strategy: fallback

Validate before calling

// Pre-check: confirm the cleanup thread's prerequisites (db reachable) before teardown
let ok = std::process::Command::new("pg_isready").status().map(|s| s.success()).unwrap_or(false);

Try / catch

// Convert teardown panics into warnings
match handle.join() {
    Ok(()) => {},
    Err(e) => eprintln!("warning: test db cleanup thread failed: {e:?}"),
}

Prevention

When it happens

Trigger: Any panic inside the cleanup thread — 'create cleanup runtime', 'db connection should success', or 'Drop database' — reaching the caller during Drop/test teardown.

Common situations: Debugging why a test process aborts at teardown; CI runs where tests pass but the process exits non-zero due to cleanup panics.

Related errors


AI-assisted analysis of loco-rs/loco@23639d1e36 (2026-09-12). Data as JSON: /api/errors/839f94951112bf4e. Report an issue: GitHub.

Appendix: source

Thrown at src/testing/db.rs:156

        // it so cleanup fully completes before `cleanup_db` returns. This is
        // safe to call from `Drop::drop` since nothing here is `.await`ed on
        // the caller's runtime.
        std::thread::spawn(move || {
            let rt = tokio::runtime::Runtime::new().expect("create cleanup runtime");

            rt.block_on(async {
                let pool = Pool::<Postgres>::connect(&connection_string)
                    .await
                    .expect("db connection should success");
                let query = format!("drop database if exists {table_name};");
                sqlx::query(AssertSqlSafe(query))
                    .execute(&pool)
                    .await
                    .expect("Drop database");
            });
        })
        .join()
        .expect("db cleanup thread panicked");
    }
}

pub struct SqliteTest {
    connection_string: String,
    db_folder: PathBuf,
    _tree: tree_fs::Tree, // Keep the tree alive while the test runs
}

impl SqliteTest {
    /// Prepare new `SQLite` connection string.
    ///
    /// # Errors
    /// Returns an error if could not prepare the connection string
    pub fn new(conn_str: &str) -> Result<Self> {
        let db_name = db::extract_db_name(conn_str)?;

        let tree = TreeBuilder::default()

View on GitHub (pinned to 23639d1e36)