{"record":{"id":"9c7f8f43e78fb832","repo":"gfx-rs/wgpu","slug":"greater-than-2-32-nested-error-scopes","errorCode":null,"errorMessage":"Greater than 2^32 nested error scopes","messagePattern":"Greater than 2\\^32 nested error scopes","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"wgpu/src/backend/webgpu.rs","lineNumber":2766,"sourceCode":"\n    fn on_uncaptured_error(&self, handler: Arc<dyn crate::UncapturedErrorHandler>) {\n        let f = Closure::wrap(Box::new(move |event: webgpu_sys::GpuUncapturedErrorEvent| {\n            let error = error_from_js(event.error().value_of());\n            handler(error);\n        }) as Box<dyn FnMut(_)>);\n        self.inner\n            .set_onuncapturederror(Some(f.as_ref().unchecked_ref()));\n        // Release memory management of this closure from Rust to the JS GC.\n        // TODO: This will leak if weak references is not supported.\n        f.forget();\n    }\n\n    fn push_error_scope(&self, filter: crate::ErrorFilter) -> u32 {\n        let index = self.error_scope_count.get();\n        self.error_scope_count.set(\n            index\n                .checked_add(1)\n                .expect(\"Greater than 2^32 nested error scopes\"),\n        );\n        self.inner.push_error_scope(match filter {\n            crate::ErrorFilter::OutOfMemory => webgpu_sys::GpuErrorFilter::OutOfMemory,\n            crate::ErrorFilter::Validation => webgpu_sys::GpuErrorFilter::Validation,\n            crate::ErrorFilter::Internal => webgpu_sys::GpuErrorFilter::Internal,\n        });\n        index\n    }\n\n    fn pop_error_scope(&self, index: u32) -> Pin<Box<dyn dispatch::PopErrorScopeFuture>> {\n        let current_scope_count = self.error_scope_count.get();\n        let is_panicking = crate::util::is_panicking();\n        if current_scope_count == 0 && !is_panicking {\n            panic!(\"Mismatched pop_error_scope call: no error scope for this thread. Error scopes are thread-local.\");\n        }\n        if index + 1 != current_scope_count && !is_panicking {\n            panic!(\n                \"Mismatched pop_error_scope call: error scopes must be popped in reverse order.\"","sourceCodeStart":2748,"sourceCodeEnd":2784,"githubUrl":"https://github.com/gfx-rs/wgpu/blob/3e11ff59bf3f9795d285ecc045014089640d7248/wgpu/src/backend/webgpu.rs#L2748-L2784","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nfor item in items {\n    device.push_error_scope(ErrorFilter::Validation);\n    if item.invalid { continue; } // pop never runs, counter leaks\n    draw(item);\n    device.pop_error_scope(ErrorFilter::Validation);\n}\n// after\nfor item in items {\n    device.scope(|scope| { // RAII: pop is guaranteed\n        if !item.invalid {\n            draw(scope, item);\n        }\n    });\n}","handlingStrategy":"validation","validationCode":"fn assert_scopes_balanced(pushed: usize, popped: usize) {\n    assert_eq!(pushed, popped,\n        \"unbalanced error scopes: {} pushed, {} popped\",\n        pushed, popped);\n}\n// track counts in debug builds:\nlet mut depth = 0usize;\nfn push(device: &Device, depth: &mut usize) {\n    device.push_error_scope(ErrorFilter::Validation);\n    *depth += 1;\n}\nfn pop(device: &Device, depth: &mut usize, filter: ErrorFilter)\n    -> impl Future<Output = Option<Error>> + '_\n{\n    *depth = depth.saturating_sub(1);\n    device.pop_error_scope(filter)\n}","typeGuard":null,"tryCatchPattern":"// the panic is not catchable in normal Rust; guard the pattern instead.\n// Prefer RAII scope API so balance is guaranteed:\ndevice.scope(|scope| {\n    // ... GPU work ...\n}); // pop_error_scope is issued automatically","preventionTips":["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"],"tags":["webgpu","error-scopes","panic","overflow","nesting"],"backgroundTag":"unbalanced-error-scopes","analyzedSha":"3e11ff59bf3f9795d285ecc045014089640d7248","analyzedAt":"2026-09-03T01:43:21.459Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T07:17:11.731Z"}