dbt-labs/dbt-core · error
`EnterGuard` values dropped out of order. Guards returned by
Error message
`EnterGuard` values dropped out of order. Guards returned by `Handle::enter()` must be dropped in the reverse order as they were acquired.
What it means
This EnterGuard tracks the depth of a thread-local task context stack (like tokio's Handle::enter). Guards must be dropped in LIFO order; if a guard's depth doesn't match the current stack depth at drop time, the nesting invariant is violated and the code panics (unless already panicking, in which case it stays silent).
Source
Thrown at crates/dbt-runtime/src/context/current.rs:80
}
impl HandleCell {
pub(super) const fn new() -> HandleCell {
HandleCell {
handle: RefCell::new(None),
depth: Cell::new(0),
}
}
}
impl Drop for SetCurrentGuard {
fn drop(&mut self) {
CONTEXT.with(|ctx| {
let depth = ctx.current.depth.get();
if depth != self.depth {
if !std::thread::panicking() {
panic!(
"`EnterGuard` values dropped out of order. Guards returned by \
`Handle::enter()` must be dropped in the reverse order as they \
were acquired."
);
} else {
// Just return... this will leave handles in a wonky state though...
return;
}
}
*ctx.current.handle.borrow_mut() = self.prev.take();
ctx.current.depth.set(depth - 1);
});
}
}
View on GitHub (pinned to 0267ce9170)
Solutions
- Scope each enter() guard tightly (let _guard = handle.enter(); inside a block) so RAII guarantees reverse-order drop.
- Never store EnterGuard values in collections or fields; only keep them as local bindings.
- Check for mem::forget, ManuallyDrop, or early returns that skip guard drops out of order.
- If guards must nest, ensure the innermost guard is dropped before the outer one.
Example fix
// before
let g1 = handle.enter();
let g2 = handle.enter();
drop(g1); // wrong order
drop(g2);
// after
let g1 = handle.enter();
{
let g2 = handle.enter();
// ... inner work ...
} // g2 dropped first automatically
drop(g1); Defensive patterns
Strategy: type-guard
Type guard
fn guard_depth_ok(ctx: &Ctx, guard: &EnterGuard) -> bool {
ctx.current.depth.get() == guard.depth + 1
} Try / catch
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _g = handle.enter();
run_task();
})).unwrap_or_else(|_| eprintln!("enter scope panicked; guards unwound in order")); Prevention
- Keep EnterGuard values as local RAII bindings; never store them in structs, Vecs, or fields.
- Scope nested enters in inner blocks so inner guards drop first automatically.
- Avoid mem::forget/ManuallyDrop on guards and early returns that juggle multiple guards manually.
- Prefer passing the Handle to code that needs it instead of re-entering repeatedly.
When it happens
Trigger: Dropping an EnterGuard out of acquisition order — e.g. storing guards in a collection and dropping them in FIFO order, leaking one guard while dropping an outer one, mem::forget on a guard, or holding a guard across an early return/panic path that unwinds past an inner guard differently.
Common situations: Manually calling Handle::enter() in tests or FFI boundaries and juggling multiple guards; wrapping enter() in a non-RAII pattern; catching a panic inside an entered scope and then dropping guards in the wrong sequence.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- {e} {:?}
- {e}
- inconsistent park state; actual = {actual}
- inconsistent park_timeout state; actual = {actual}
- Schema not found for canonical FQN: {}
AI-assisted analysis of dbt-labs/dbt-core@0267ce9170 (2026-09-07).
Data as JSON: /api/errors/8495c20c49964150.
Report an issue: GitHub.