#440

fix: show the error message in preview overlays on Firefox

OpenParthKapoor-devwants to merge intofrom

PR type

development

Release impact

changeset — .changeset/olive-pumas-shake.md

What happens today

Every example preview on https://vgpu.sh fails in Firefox with an overlay that contains a stack and nothing else — no message, no indication of what went wrong:

Preview error
r@https://vgpu.sh/_next/static/immutable/chunks/25i9s9l19xma4.js:1:6014
t@https://vgpu.sh/_next/static/immutable/chunks/25i9s9l19xma4.js:1:15326
@https://vgpu.sh/_next/static/immutable/chunks/25i9s9l19xma4.js:1:34847
d@https://vgpu.sh/_next/static/immutable/chunks/25i9s9l19xma4.js:1:39474
...

To reproduce, open any /preview/<slug> in a Firefox profile without dom.webgpu.enabled (the default on Linux today).

Two separate causes stack up.

1. The overlay drops the message

apps/docs/app/preview/[slug]/example-canvas.tsx formatted the error as:

return error instanceof Error ? error.stack || error.message : String(error);

V8 puts name: message on the first line of error.stack, so this reads correctly in Chrome. SpiderMonkey and JavaScriptCore emit call frames only, so error.stack never contains the message and the || error.message fallback never fires. Measured in Firefox 151, for a thrown Error:

{
  "message": "navigator.gpu.requestAdapter() returned null.",
  "stackFirstLine": "VGPUError@https://vgpu.sh/preview/simple-gradient:2:7",
  "stackIncludesMessage": false
}

This one function feeds both the in-iframe overlay and the postMessage payload that components/example-preview.tsx renders on the docs page, so both lost the message.

2. The underlying error blamed the wrong thing

requestBrowserDevice optional-chained the request:

const adapter = await nav.gpu?.requestAdapter({ powerPreference: opts.powerPreference });
if (!adapter) throw unsupportedError("init", "navigator.gpu.requestAdapter() returned null.");

When navigator.gpu is absent the optional chain yields undefined, so a browser that exposes no WebGPU at all was reported as an adapter that came back null — sending people to look for a driver or hardware problem instead of a missing API.

The change

  • Extract the formatting into apps/docs/lib/preview-error-message.ts so it is unit-testable, and compose name: message ahead of the stack unless the engine already supplied it. V8 output is byte-identical; Firefox and Safari gain the headline.
  • Guard navigator.gpu in requestBrowserDevice and throw WebGPU is not available in this browser. — the same wording apps/docs/lib/browser-device.ts and the three.js examples already use, and the string docs/topics/agent-browser-webgpu.docs.md already tells readers to grep overlays for. A genuinely null adapter keeps its existing message.

With both applied, the same Firefox profile shows VGPUError: WebGPU is not available in this browser. above the frames instead of frames alone.

Verification

  • pnpm typecheck — passes.
  • pnpm test:fast — 1208 passed, 76 skipped, 0 failed.
  • pnpm migrations:check — passes.
  • pnpm docs:verify-snippets — snippets=623 tsc=OK.
  • pnpm check:filenames — passes.
  • New tests: 6 in apps/docs/lib/preview-error-message.test.ts, 2 in packages/vgpu-api/tests/init-device-options.test.ts. Reverting either fix turns 5 of them red, and the V8-path and null-adapter tests stay green both ways.
  • Firefox 151 over WebDriver BiDi, used both to capture the failure above and to confirm the new formatter puts the message on line one.

Bundle budget

vgpu experience full-root is a hard client gate and is already close to it, so measuring the delta seemed worth reporting rather than leaving to CI:

gzipbudgetheadroom
canary41846 B41984 B138 B
this PR41871 B41984 B113 B

+25 B, from the one added guard and its message. Happy to fold the message into the existing requestAdapter throw if you would rather not spend the bytes, though that costs the distinction between the two failures.

The four tooling budgets that pnpm bundle-check warns about (vgpu/node, vgpu/mock, both wgsl loaders) are over on canary already and unchanged here.

Notes

  • Targets canary per CONTRIBUTING: the change is not web-only, since it touches packages/vgpu-api as well as apps/docs.
  • The two halves are independent — if you would prefer the docs overlay fix and the init() message as separate PRs, say the word and I will split them.
  • This is a different Firefox problem from #385, which is a naga validation failure on workgroup pointers in the FFT ocean demo.

🤖 Generated with Claude Code

Activity

3
vercel[bot]2mo ago

@ParthKapoor-dev is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

ParthKapoor-dev2mo ago

Heads up on the red release-impact check — it is failing before it reads anything in this PR, so it is not a declaration problem.

The job's checkout step stops with:

Refusing to check out fork pull request code from a 'pull_request_target' workflow. [...] To opt in, review the risks at https://gh.io/securely-using-pull_request_target and set 'allow-unsafe-pr-checkout: true' on the actions/checkout step.

That is actions/checkout's guard against pwn-request patterns, and it fires on any fork-authored PR, so the gate cannot currently go green on an external contribution regardless of what the description says. Flagging it because CONTRIBUTING describes release-impact as a required check on canary.

Worth noting the workflow already treats the candidate as data rather than executing it — the step is literally named "Checkout candidate as data only" — so the safe fix is likely fetching the candidate blobs without a working-tree checkout (or actions/checkout with persist-credentials: false plus the explicit opt-in, given nothing from the fork is executed). Happy to leave that to you; I did not want the red check read as a missing declaration on my side.

For the record, the description does carry both required sections: ## PR type → development, and ## Release impact → changeset — .changeset/olive-pumas-shake.md.

The Vercel check is the usual "Authorization required to deploy" that every fork PR gets.