gfx-rs/wgpu · critical

Mismatched pop_error_scope call: error scopes must be popped

Error message

Mismatched pop_error_scope call: error scopes must be popped in reverse order.

What it means

Error scopes must be popped in LIFO (reverse) order. This panic fires when pop_error_scope is given/encounters an index that is not the top of the thread's scope stack, i.e. an inner scope is still open while an outer one is being popped.

Source

Thrown at wgpu-core/src/error.rs:405

        // and we are supposed to just drop the error scope on the floor.
        let is_panicking = is_panicking();
        let thread_id = thread_id::ThreadId::current();
        let err = "Mismatched pop_error_scope call: no error scope for this thread. Error scopes are thread-local.";
        let scopes = match error_sink.scopes.get_mut(&thread_id) {
            Some(s) => s,
            None => {
                if !is_panicking {
                    panic!("{err}");
                } else {
                    return None;
                }
            }
        };
        if scopes.is_empty() && !is_panicking {
            panic!("{err}");
        }
        if index as usize != scopes.len() - 1 && !is_panicking {
            panic!(
                "Mismatched pop_error_scope call: error scopes must be popped in reverse order."
            );
        }

        // It would be more correct in this case to use `remove` here so that when unwinding is occurring
        // we would remove the correct error scope, but we don't have such a primitive on the web
        // and having consistent behavior here is more important. If you are unwinding and it unwinds
        // the guards in the wrong order, it's totally reasonable to have incorrect behavior.
        let scope = match scopes.pop() {
            Some(s) => s,
            None if !is_panicking => unreachable!(),
            None => return None,
        };

        scope.error
    }
}

View on GitHub (pinned to 3e11ff59bf)

Solutions

  1. Restructure so scopes are strictly nested: always pop the most recently pushed scope first.
  2. Close all inner scopes (await their pops) before popping the outer scope.
  3. Avoid sharing a thread's scope stack across interleaved async tasks; keep push/pop within a single async block.
  4. Flatten to one scope level per function to make LIFO order trivially correct.

Example fix

// before
device.push_error_scope(outer);
device.push_error_scope(inner);
device.pop_error_scope().await; // pops inner? index mismatch panics if popping outer
// after
device.push_error_scope(outer);
device.push_error_scope(inner);
device.pop_error_scope().await; // inner first
device.pop_error_scope().await; // then outer
Defensive patterns

Strategy: validation

Validate before calling

// enforce LIFO with a scope stack abstraction
let guard = ScopeGuard::push(&device, ErrorFilter::Validation)?; // inner scopes as nested guards
// popping happens automatically in drop order = LIFO

Type guard

struct ScopeGuard<'a> { device: &'a Device, done: bool }
impl<'a> ScopeGuard<'a> {
    fn inner(&'a self, k: ErrorFilter) -> ScopeGuard<'a> {
        self.device.push_error_scope(k);
        ScopeGuard { device: self.device, done: false }
    }
}
impl Drop for ScopeGuard<'_> { fn drop(&mut self) { /* pop via runtime */ } }

Prevention

When it happens

Trigger: Popping an outer scope while an inner push_error_scope is still open; interleaving scope lifetimes across tasks so scopes overlap incorrectly; popping scopes out of nesting order in nested validation regions.

Common situations: Nested error-scope usage (scope inside scope) where the inner pop was forgotten or deferred; concurrent async tasks each opening scopes on the same thread executor and interleaving pushes/pops; helper functions that close the wrong scope.

Related errors


AI-assisted analysis of gfx-rs/wgpu@3e11ff59bf (2026-09-03). Data as JSON: /api/errors/4f67340d393f6635. Report an issue: GitHub.