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
- Restructure so scopes are strictly nested: always pop the most recently pushed scope first.
- Close all inner scopes (await their pops) before popping the outer scope.
- Avoid sharing a thread's scope stack across interleaved async tasks; keep push/pop within a single async block.
- 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
- Prefer one scope level per function; avoid manual nested pops.
- Use drop-order (RAII) to guarantee LIFO scope closure.
- Never interleave scope lifetimes across concurrently scheduled tasks on one thread.
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
- wgpu error: {err}
- Mismatched pop_error_scope call: no error scope for this thr
- Mismatched pop_error_scope call: error scopes must be popped
- ${name} is not webgpu
- No context available. You need to enable one of wgpu's backe
AI-assisted analysis of gfx-rs/wgpu@3e11ff59bf (2026-09-03).
Data as JSON: /api/errors/4f67340d393f6635.
Report an issue: GitHub.