Debugging WebGPU
Error handling and debugging tools
WebGPU's validation layer catches mistakes that would cause undefined behavior on the GPU. This is a significant improvement over older APIs where errors manifested as silent corruption, driver crashes, or security vulnerabilities. Understanding how to read error messages and use debugging tools transforms frustrating debugging sessions into straightforward problem-solving.
The Validation Layer
Every WebGPU call passes through a validation layer before reaching the GPU. This layer checks that arguments are valid, resources exist, and operations make sense. When validation fails, you get an error message describing the problem.
// This will produce a validation error
const buffer = device.createBuffer({
size: 0, // Error: size must be > 0
usage: GPUBufferUsage.VERTEX,
});The browser console will show something like:
Uncaught DOMException: Failed to execute 'createBuffer' on 'GPUDevice':
Buffer size must be greater than 0.Validation happens on the CPU before any GPU work, so errors appear immediately. This differs from GPU execution errors, which may surface later (or not at all—the GPU often just produces garbage output).
Interactive: Common error messages
device.createBuffer({
size: 0,
usage: GPUBufferUsage.VERTEX,
});Buffer size (0) must be greater than 0.You cannot create a zero-sized buffer. Every buffer must have at least one byte.
device.createBuffer({
size: 64, // Provide actual size
usage: GPUBufferUsage.VERTEX,
});Click through different errors to learn common patterns and their fixes.
Reading Error Messages
WebGPU error messages follow a pattern: they identify the operation that failed and explain why. Learn to parse them:
"Failed to execute 'X' on 'Y'" — The operation X on object Y failed. Look at the arguments you passed to X.
"...must be..." — A parameter violated a constraint. The message tells you what constraint.
"...is invalid..." — You referenced something that doesn't exist or was destroyed.
"...not in the allowed set..." — You used a value not permitted in that context.
Some errors reference validation rules by ID. The WebGPU specification defines these rules, and you can look them up for detailed explanations:
Validation rule [Buffer-Usage] violated:
COPY_DST usage required for writeBufferError Scopes
For finer control over error handling, use error scopes. These let you catch errors from a specific section of code:
device.pushErrorScope('validation');
// Operations that might fail
const buffer = device.createBuffer({ ... });
const texture = device.createTexture({ ... });
device.popErrorScope().then(error => {
if (error) {
console.error('Creation failed:', error.message);
}
});Error scopes are useful for:
- Testing whether operations succeed without crashing
- Implementing fallback behavior when optional features fail
- Collecting errors for logging/telemetry
The scope types are validation (API misuse), out-of-memory (resource allocation failed), and internal (driver/implementation bugs).
Interactive: Validation in action
Watch validation catch errors in real-time. Failed operations show what went wrong, allowing you to fix and retry.
Browser Developer Tools
Modern browsers include WebGPU debugging support in their developer tools.
Chrome DevTools shows WebGPU calls in the Console and allows inspecting GPU objects. Enable "Verbose" logging level to see all API calls.
Firefox has similar support, with GPU process information visible in about:support.
Third-party tools like WebGPU Inspector provide deeper inspection:
- View all created resources
- Inspect buffer contents
- See bind group layouts
- Capture and replay frames
To use WebGPU Inspector, install the browser extension, then click its icon while on a page using WebGPU. You'll see a panel listing all GPU objects and their state.
Common Mistakes
Certain errors appear repeatedly when learning WebGPU. Recognizing them saves debugging time.
Interactive: Test your debugging knowledge
const buffer = device.createBuffer({
size: 48,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});Forgetting to map/unmap buffers:
// Wrong: trying to write before mapping
buffer.getMappedRange(); // Error if not mapped
// Right: map first
await buffer.mapAsync(GPUMapMode.WRITE);
const range = buffer.getMappedRange();
// ... write data ...
buffer.unmap();Mismatched bind group layouts:
// Pipeline expects layout A
const pipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [layoutA]
}),
...
});
// But you pass a bind group created with layout B
renderPass.setBindGroup(0, bindGroupWithLayoutB); // ErrorBuffer size not aligned:
// Wrong: 13 bytes, but uniform buffers need 16-byte alignment
const buffer = device.createBuffer({
size: 13,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
}); // May work, but causes issues
// Right: round up to alignment
const buffer = device.createBuffer({
size: Math.ceil(13 / 16) * 16, // 16 bytes
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});Using destroyed resources:
buffer.destroy();
// ... later ...
queue.writeBuffer(buffer, 0, data); // Error: buffer is destroyedWorkgroup size mismatch:
// Shader declares workgroup size 64
@compute @workgroup_size(64)
fn main() { ... }// But dispatch assumes workgroup size 256
const totalThreads = 1024;
pass.dispatchWorkgroups(totalThreads / 256); // Wrong: only 4 workgroups
pass.dispatchWorkgroups(totalThreads / 64); // Right: 16 workgroupsDebugging Shaders
Shader bugs are harder to diagnose because you can't use console.log(). Strategies:
Output to a buffer: Write intermediate values to a storage buffer, read them back on CPU.
@group(0) @binding(0) var<storage, read_write> debug: array<f32>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3u) {
let value = someComputation();
debug[id.x] = value; // Capture for debugging
}Visualize with color: In fragment shaders, output intermediate values as colors.
@fragment
fn main(@location(0) uv: vec2f) -> @location(0) vec4f {
let normal = computeNormal(uv);
// Debug: show normals as RGB
return vec4f(normal * 0.5 + 0.5, 1.0);
// Real output (commented during debugging):
// return computeLighting(normal);
}Simplify: Comment out sections until the bug disappears. The last section you commented out contains the problem.
Check bounds: Many shader bugs come from out-of-bounds array access. Add explicit bounds checks during debugging:
fn safeRead(index: u32) -> f32 {
if (index >= arrayLength(&data)) {
return -999.0; // Obviously wrong value for debugging
}
return data[index];
}Device Lost
Sometimes the GPU device becomes unavailable—driver crash, GPU reset, or resource exhaustion. Handle this gracefully:
device.lost.then(info => {
console.error(`Device lost: ${info.message}`);
if (info.reason === 'destroyed') {
// You called device.destroy()
} else {
// Unexpected loss—try to recover
reinitializeWebGPU();
}
});Device loss destroys all GPU resources. Your only option is to recreate the device and all resources from scratch. Design your application to support this from the start, rather than retrofitting recovery logic later.
Interactive: Debug checklist
Use this checklist when debugging. Click items as you verify them.
Defensive Programming
Catch problems early with assertions and checks:
function createUniformBuffer(device: GPUDevice, data: ArrayBuffer) {
// Assert alignment
if (data.byteLength % 16 !== 0) {
console.warn(`Uniform buffer size ${data.byteLength} not 16-byte aligned`);
}
const buffer = device.createBuffer({
size: Math.ceil(data.byteLength / 16) * 16,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(buffer, 0, data);
return buffer;
}Wrapper functions that validate inputs prevent classes of errors entirely. The upfront cost of writing them pays off in faster debugging.
Key Takeaways
- WebGPU's validation layer catches errors before they reach the GPU—read error messages carefully
- Use error scopes to handle errors from specific code sections without crashing
- Browser dev tools and extensions like WebGPU Inspector help visualize GPU state
- Common mistakes: unmapped buffers, mismatched layouts, alignment issues, destroyed resources
- Debug shaders by writing to buffers, visualizing as colors, or simplifying until bugs disappear
- Handle device loss from the start—design applications to support full reinitialization