The content timeline (your JavaScript thread) checks only what it can decide at once, such as a misspelled enum or a missing member, and those fail as exceptions. Anything needing the device's state (usages, limits, formats) is checked on the device timeline in the GPU process (Encoders and Queues): the call returns at once, possibly with an invalid object, and the error arrives later as a GPUError.

<script type="module">
const device = await (await navigator.gpu.requestAdapter()).requestDevice();
device.onuncapturederror = (e) => console.log(`later: ${e.error.message.split('\n')[0]}`);
const attempt = (what, fn) => { try { fn(); console.log(`${what}: returned`); }
catch (e) { console.log(`${what}: threw ${e.name}: ${e.message.split(': ').pop()}`); } };
attempt('no size', () => device.createBuffer({ usage: GPUBufferUsage.COPY_DST }));
attempt('mapped 6 B', () => device.createBuffer({ size: 6, usage: 8,
mappedAtCreation: true }));
attempt('usage 0', () => device.createBuffer({ size: 4, usage: 0 }));
const buffer = device.createBuffer({ size: 4, usage: GPUBufferUsage.COPY_DST });
attempt('16 B into 4', () => device.queue.writeBuffer(buffer, 0, new Float32Array(4)));
</script>no size: threw TypeError: Required member is undefined. mapped 6 B: threw RangeError: createBuffer failed, size (6) is not a multiple of 4 when mappedAtCreation == true usage 0: returned 16 B into 4: returned later: Buffer usages must not be 0. later: Write range (bufferOffset: 0, size: 16) does not fit in [Buffer (unlabeled)] size (4).
Waiting on the GPU process for each call would stall the page, so invalidity spreads instead: a bind group made from an invalid buffer is invalid, as is a command buffer using it.