Push and Pop Error Scopes

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:

Nested error scopes, one error per scope and an unbalanced popHTMLLive
<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>

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).