device.pushErrorScope(filter) pushes a scope onto the device's error-scope stack, and device.popErrorScope() removes it and resolves to the error it caught, or null. The filter is 'validation', 'out-of-memory' or 'internal'; an error goes to the innermost scope whose filter matches its type, and becomes an uncapturederror event only when none matches. Scopes nest:
<script type="module">
const device = await (await navigator.gpu.requestAdapter()).requestDevice();
device.pushErrorScope('out-of-memory'); // outer scope
device.pushErrorScope('validation'); // inner scope
device.createBuffer({ label: 'first', size: 4, usage: 0 });
device.createBuffer({ label: 'second', size: 4, usage: 0 });
const inner = await device.popErrorScope(), outer = await device.popErrorScope();
console.log(`inner: ${inner.message.split('\n')[1]}\nouter: ${outer}`);
device.popErrorScope().catch((e) => console.log(`extra pop: ${e.name}: ${e.message}`));
</script>inner: - While calling [Device].CreateBuffer([BufferDescriptor ""first""]). outer: null extra pop: OperationError: No error scopes to pop
A scope reports one error (Chrome 1 kept the first), so wrap small groups of calls, as checked() does (Feature Detection). The stack belongs to the device, not your function: await between push and pop, and code that runs meanwhile, such as the render loop, records its errors into your scope. Keep each pair in one synchronous stretch, and in hot paths pop without awaiting: device.popErrorScope().then(report).