gfx-rs/wgpu · error

Greater than 2^32 nested error scopes

Error message

Greater than 2^32 nested error scopes

What it means

This panic comes from wgpu's WebGPU backend bookkeeping: every Device::push_error_scope increments a u32 counter of nested scopes, and the library deliberately panics if the count would exceed u32::MAX (2^32). It means push_error_scope has been called astronomically many more times than pop_error_scope, i.e. error scopes are severely unbalanced or leaking in a long-running loop.

Source

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

    fn on_uncaptured_error(&self, handler: Arc<dyn crate::UncapturedErrorHandler>) {
        let f = Closure::wrap(Box::new(move |event: webgpu_sys::GpuUncapturedErrorEvent| {
            let error = error_from_js(event.error().value_of());
            handler(error);
        }) as Box<dyn FnMut(_)>);
        self.inner
            .set_onuncapturederror(Some(f.as_ref().unchecked_ref()));
        // Release memory management of this closure from Rust to the JS GC.
        // TODO: This will leak if weak references is not supported.
        f.forget();
    }

    fn push_error_scope(&self, filter: crate::ErrorFilter) -> u32 {
        let index = self.error_scope_count.get();
        self.error_scope_count.set(
            index
                .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."

View on GitHub (pinned to 3e11ff59bf)

Solutions

  1. Ensure every push_error_scope call is paired with exactly one pop_error_scope call on all code paths, including early returns and panics
  2. Prefer device.scope(|scope| ...) which balances push/pop automatically via RAII
  3. When using pop_error_scope's future, always await it or propagate it; never discard a polled scope future
  4. Restructure per-item scope wrapping in loops: wrap the whole batch once, or pop inside the same iteration
  5. Check for early `?`/return statements between the push and the pop in your scoped code

Example fix

// before
for item in items {
    device.push_error_scope(ErrorFilter::Validation);
    if item.invalid { continue; } // pop never runs, counter leaks
    draw(item);
    device.pop_error_scope(ErrorFilter::Validation);
}
// after
for item in items {
    device.scope(|scope| { // RAII: pop is guaranteed
        if !item.invalid {
            draw(scope, item);
        }
    });
}
Defensive patterns

Strategy: validation

Validate before calling

fn assert_scopes_balanced(pushed: usize, popped: usize) {
    assert_eq!(pushed, popped,
        "unbalanced error scopes: {} pushed, {} popped",
        pushed, popped);
}
// track counts in debug builds:
let mut depth = 0usize;
fn push(device: &Device, depth: &mut usize) {
    device.push_error_scope(ErrorFilter::Validation);
    *depth += 1;
}
fn pop(device: &Device, depth: &mut usize, filter: ErrorFilter)
    -> impl Future<Output = Option<Error>> + '_
{
    *depth = depth.saturating_sub(1);
    device.pop_error_scope(filter)
}

Try / catch

// the panic is not catchable in normal Rust; guard the pattern instead.
// Prefer RAII scope API so balance is guaranteed:
device.scope(|scope| {
    // ... GPU work ...
}); // pop_error_scope is issued automatically

Prevention

When it happens

Trigger: Calling device.push_error_scope(...) inside a loop (or per-frame/per-entity) without the matching pop_error_scope ever executing, e.g. early-returns, panics in the scoped section, or spawning a task that polls the scope future and dropping it, repeated billions of times.

Common situations: Long-running GPU apps (games, compute jobs) that wrap each draw or each item in an error scope but skip popping on certain code paths; relying on dropping the scope future instead of awaiting it; API misuse where the WebGPU browser backend (wasm) never implicitly balances scopes for you.

Related errors


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