gfx-rs/wgpu · error

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

In the WebGPU backend, error scopes are tracked per-thread with a counter; pop_error_scope must be called for the innermost (most recently pushed) scope first. wgpu panics when the scope being popped does not match the current innermost scope, i.e. scopes are popped out of order.

Source

Thrown at wgpu/src/backend/webgpu.rs:2783

                .checked_add(1)
                .expect("Greater than 2^32 nested error scopes"),
        );
        self.inner.push_error_scope(match filter {
            crate::ErrorFilter::OutOfMemory => webgpu_sys::GpuErrorFilter::OutOfMemory,
            crate::ErrorFilter::Validation => webgpu_sys::GpuErrorFilter::Validation,
            crate::ErrorFilter::Internal => webgpu_sys::GpuErrorFilter::Internal,
        });
        index
    }

    fn pop_error_scope(&self, index: u32) -> Pin<Box<dyn dispatch::PopErrorScopeFuture>> {
        let current_scope_count = self.error_scope_count.get();
        let is_panicking = crate::util::is_panicking();
        if current_scope_count == 0 && !is_panicking {
            panic!("Mismatched pop_error_scope call: no error scope for this thread. Error scopes are thread-local.");
        }
        if index + 1 != current_scope_count && !is_panicking {
            panic!(
                "Mismatched pop_error_scope call: error scopes must be popped in reverse order."
            );
        }
        // Decrement the error scope count. We've asserted that the current
        // size is `index + 1` above.
        self.error_scope_count.set(index);

        let error_promise = self.inner.pop_error_scope();
        Box::pin(MakeSendFuture::new(
            wasm_bindgen_futures::JsFuture::from(error_promise),
            future_pop_error_scope,
        ))
    }

    unsafe fn start_graphics_debugger_capture(&self) {
        // No capturing api in webgpu
    }

View on GitHub (pinned to 3e11ff59bf)

Solutions

  1. Pop error scopes in strict LIFO order: always pop the most recently pushed scope first on that thread.
  2. Restructure code so each scope is popped at the end of the same block where it was pushed (RAII-style nesting).
  3. Ensure the scope handle passed to pop_error_scope belongs to the current thread and is the active innermost one.
  4. Avoid interleaving push/pop calls across helper functions that assume a different nesting depth.

Example fix

// before
let outer = device.push_error_scope(ErrorFilter::Validation);
let inner = device.push_error_scope(ErrorFilter::OutOfMemory);
let err = outer.pop().await; // WRONG: outer is not innermost
// after
let err = inner.pop().await; // pop innermost first
let err2 = outer.pop().await;
Defensive patterns

Strategy: validation

Validate before calling

fn can_pop(scope_depth: usize, current: usize) -> bool { current > 0 && scope_depth == current }
// pop only if can_pop(index, device.error_scope_depth())

Try / catch

// panics are not catchable safely in Rust; avoid by validation.
// If using catch_unwind, ensure no error scopes are left open.

Prevention

When it happens

Trigger: Calling device.pop_error_scope() (or queue.pop_error_scope()) on a scope that is not the most recently pushed one on the same thread — e.g. pushing scope A, pushing scope B, then popping A before B.

Common situations: Tracking multiple scope handles in a collection and popping them in the wrong order; popping a scope from a different thread than the one that pushed it (scopes are thread-local); popping an outer scope while an inner scope is still open.

Related errors


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