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
- Ensure every push_error_scope call is paired with exactly one pop_error_scope call on all code paths, including early returns and panics
- Prefer device.scope(|scope| ...) which balances push/pop automatically via RAII
- When using pop_error_scope's future, always await it or propagate it; never discard a polled scope future
- Restructure per-item scope wrapping in loops: wrap the whole batch once, or pop inside the same iteration
- 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
- Always use device.scope() instead of manual push/pop when possible
- Never `continue`/`return`/`?` between push_error_scope and pop_error_scope
- Always await the future returned by pop_error_scope; never drop it unawaited
- In debug builds assert push/pop counts match per frame or per unit of work
- Avoid opening a new error scope per item in hot loops; scope once around the batch
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
- slice offset {slice_offset} size {slice_size} is out of rang
- Unexpected error: constructor={constructor} value={value}
- {:?} is not enabled for this backend
- VERTEX_ATTRIBUTE_64BIT feature must be enabled to use Double
- Mismatched pop_error_scope call: error scopes must be popped
AI-assisted analysis of gfx-rs/wgpu@3e11ff59bf (2026-09-03).
Data as JSON: /api/errors/9c7f8f43e78fb832.
Report an issue: GitHub.