cloudflare/quiche · error

tokio task ID already in use

Error message

tokio task ID already in use: {id}

What it means

task-killswitch's add_task_if panics when a tokio task::Id is inserted into the registry while a live (non-tombstone) entry with that same ID already exists. Task IDs should be unique per spawned task, so this indicates a bookkeeping bug: the same ID was added twice without being removed, i.e. an internal invariant violation of the registry.

Solutions

  1. Always pair every add_task_if with a remove_task (e.g. via the provided JoinHandle wrappers) so IDs are freed before reuse
  2. Check the map before inserting; treat an existing live entry as a bug and log/replace it deliberately
  3. Update task-killswitch / tokio to compatible versions if ID reuse behavior changed
  4. If the duplicate is intentional (task respawned), remove the stale entry first

Example fix

// before
registry.add_task_if(id, handle, pred)?;
registry.add_task_if(id, new_handle, pred)?; // panics: id still live
// after
registry.add_task_if(id, handle, pred)?;
registry.remove_task(id);
registry.add_task_if(id, new_handle, pred)?;
Defensive patterns

Strategy: try-catch

Validate before calling

// Rust: check before registering
if registry.contains_live_task(&id) {
    registry.remove_task(id);
}
registry.add_task_if(id, handle, pred)?;

Type guard

fn is_registered(map: &HashMap<task::Id, TaskEntry>, id: &task::Id) -> bool {
    matches!(map.get(id), Some(TaskEntry::Live(_)))
}

Try / catch

// task-killswitch panics on duplicate ID; there is no catchable error.
// Guard the call site and treat a panic as a bug:
let result = std::panic::catch_unwind(|| registry.add_task_if(id, handle, pred));
if result.is_err() {
    // duplicate task id: log and recover
}

Prevention

When it happens

Trigger: Calling add_task_if (via the killswitch tracking wrapper) with a task::Id that already maps to a live TaskEntry; re-registering a task handle without a prior remove_task; racing registration logic that reuses IDs.

Common situations: Custom task-management code layered on the killswitch that spawns and re-registers handles manually, tests that add the same mocked task ID twice, upstream tokio behavior changes causing ID reuse.

Related errors


AI-assisted analysis of cloudflare/quiche@9f96daa2c2 (2026-09-08). Data as JSON: /api/errors/11d0bc7ac3b190da. Report an issue: GitHub.

Appendix: source

Thrown at task-killswitch/src/lib.rs:194

    fn add_task_if(
        &self, handle: AbortHandle, cond: impl FnOnce() -> bool,
    ) -> Result<(), AbortHandle> {
        use dashmap::Entry::*;
        let id = handle.id();

        match self.tasks.entry(id) {
            Vacant(e) => {
                if !cond() {
                    return Err(handle);
                }
                e.insert(TaskEntry::Handle(handle));
            },
            Occupied(e) if matches!(e.get(), TaskEntry::Tombstone) => {
                // Task was removed before it was added. Clear the map entry and
                // drop the handle.
                e.remove();
            },
            Occupied(_) => panic!("tokio task ID already in use: {id}"),
        }

        Ok(())
    }

    fn remove_task(&self, id: task::Id) {
        use dashmap::Entry::*;
        match self.tasks.entry(id) {
            Vacant(e) => {
                // Task was not added yet, set a tombstone instead.
                e.insert(TaskEntry::Tombstone);
            },
            Occupied(e) if matches!(e.get(), TaskEntry::Tombstone) => {},
            Occupied(e) => {
                e.remove();
            },
        }
    }

View on GitHub (pinned to 9f96daa2c2)