WASM: codeFrameColumns rejects `color: boolean` and `maxWidth: undefined`, which the napi binding and Next's own callers accept
Link to the code that reproduces this issue
https://github.com/vtamara/wasm-code-frame-columns
To Reproduce
create-next-app -e reproduction-template with next@canary. The only changes are the ones that
force the WASM SWC bindings (NEXT_TEST_WASM=1 and NEXT_TEST_WASM_DIR=node_modules/@next/swc-wasm-nodejs,
the switches Next's own test runner uses) so the bug reproduces on any OS; the WASM bindings are
the default where there are no native ones (openbsd/x64, or a failed native download).
npm installnpm run repro— callscodeFrameColumnswith the four option shapes Next itself passes and prints the result of each one. Three of them throw; the invalid ones are the shapes used bygetOriginalCodeFrame()(the dev overlay),lib/typescript/diagnosticFormatterandbuild/load-jsconfig.npm run dev(Next 16 with the WASM bindings and--webpack, since Turbopack needs the native bindings) and openhttp://localhost:3000/trigger-error, a page that throws a realENOENTwhile rendering: the server console loses the real error, and the dev overlay paints the message and the call stack but without its code frame (screenshot in the next field).- Open
http://localhost:3000/api/trigger-error, the sameENOENTon a route handler: nothing relays the error to the browser, so the real cause never reaches the console at all. - With native bindings the same commands render the code frames (the reproduction repo forces WASM, so remove the two environment variables to compare).
Current vs. Expected behavior
Expected: an error on the server side shows its code frame (the source excerpt around the throw)
in the dev overlay and in the server console, and codeFrameColumns accepts the options Next
passes it, as the native binding does.
Current: on the WASM bindings every code frame fails with a serialization error, so the real error loses its location and, in the server console, its message:
⨯ unhandledRejection: Error: Error: invalid type: boolean `false`, expected enum CodeFrameColorMode
at ignore-listed frames
GET /trigger-error 500 in 2.1s (next.js: 305ms, application-code: 1745ms)(The ENOENT: no such file or directory, open 'does-not-exist.md' of the reproduction only
appears there because the browser relays its own console.error; in the API-route case there is
no relay and the console shows only the binding error. npm run repro shows the same failure
without an app: invalid type: boolean \false`, expected enum CodeFrameColorMode, invalid type: boolean `true`, expected enum CodeFrameColorModeandinvalid type: unit value, expected usize`.)
The dev overlay paints the error message and the call stack but no code frame (and an
Unhandled Runtime Error/1 Issue badge appears in the corner):

Same server error, same overlay, with the native bindings: the frame with the source excerpt is rendered under the message.
Provide environment information
# npx --no-install next info, in the reproduction repository (the WASM bindings are forced on
# any OS, so the platform is not relevant to the bug; this is the machine where we first hit it)
Operating System:
Platform: openbsd
Arch: x64
Version: APRENDIENDODEJESUS#10
Available memory (MB): 16368
Available CPU cores: 1
Binaries:
Node: 22.20.0
npm: 10.9.3
Yarn: 1.22.22
pnpm: 11.20.0
Relevant Packages:
next: 16.4.0-canary.51 // Latest available version is detected (16.4.0-canary.51).
eslint-config-next: N/A
react: 19.3.0
react-dom: 19.3.0
typescript: 5.9.3
Next.js Config:
output: N/AWhich area(s) are affected? (Select all that apply)
SWC, Error Overlay, TypeScript
Which stage(s) are affected? (Select all that apply)
next dev (local), next build (local)
Additional context
The two bindings take different paths over the same public options contract:
napi —
crates/next-napi-bindings/src/code_frame.rsdeclares the tolerant shape and normalizes it inimpl From<NapiCodeFrameOptions> for CodeFrameOptions:color: Option<Either<NapiCodeFrameColorMode, bool>>,max_width: Option<u32>(None | Some(Either::B(false))→CodeFrameColorMode::None,Some(Either::B(true))→CodeFrameColorMode::Error,max_width.unwrap_or(DEFAULT_MAX_WIDTH)). This matches the generated type (color?: NapiCodeFrameColorMode | boolean,maxWidth?: number).wasm —
crates/wasm/src/lib.rs,code_frame_columnsdeserializes the JS value straight intoCodeFrameOptionswithserde_wasm_bindgen::from_value, andcrates/next-code-frame/src/frame.rshas noEither<_, bool>and noOptionon those fields:pub enum CodeFrameColorMode { None, Error, Warning, Info } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct CodeFrameOptions { pub color: CodeFrameColorMode, pub max_width: usize, // … }serde(default)applies to missing fields: it does not make the enum accept a boolean norusizeaccept an explicitundefined. The wasm.d.tstypes the options asany, so nothing rejects these shapes at the JS boundary.
Next's own callers use exactly the shapes the napi path documents:
packages/next/src/next-devtools/server/shared.ts→getOriginalCodeFrame()passes{ color: <isTTY>, maxWidth }, withprocess.stdout?.isTTY ?? falseas the default andmaxWidthfilled fromprocess.stdout.columns(undefined when stdout is not a TTY, which is the case fornext devwriting to a log or in CI).packages/next/src/lib/typescript/diagnosticFormatter.ts→{ color: true }.packages/next/src/build/load-jsconfig.ts→{ message, color: true }.
100% of code frames fail on the WASM path: the dev overlay, next build TypeScript diagnostics
and the jsconfig.json parse error path.
Suggested fix, either is fine and small: (1) in crates/wasm/src/lib.rs, deserialize into a
tolerant option struct (the mirror of NapiCodeFrameOptions plus its From conversion) before
calling render_code_frame; or (2) make CodeFrameOptions tolerant
(color: Option<Either<CodeFrameColorMode, bool>>, max_width: Option<usize>) and default them
where they are used. Option 1 keeps the tolerance at one place per binding boundary; option 2
makes the crate accept what the JS types already allow.
Impact: next dev on the WASM fallback cannot render the frame, so an unrelated binding error
replaces the real one — the same class of problem that #85592 fixed for @babel/code-frame
("throwing RangeErrors when rendering errors, hiding the original issue"), and #85592 lists this
exact risk ("environments where native bindings behave differently (e.g. WASM fallback path)").
next build fails with the serialization error instead of the actual TypeScript or
jsconfig.json diagnostic.
Versions: reproduced on [email protected] (with @next/[email protected]) and on
[email protected] (with @next/[email protected]); the source check above
is against canary.
Workaround while it is unfixed: a pnpm patch over
packages/next/src/shared/lib/errors/code-frame.ts that coerces the options before calling the
binding (color: false → delete the property, the default on both bindings; color: true →
'Error'; maxWidth: undefined → a number). Note that 'None' is not portable: the native
enum has no None variant, so deleting the property is what keeps both paths working. Happy to
send this as a PR if that is the preferred place for the fix.
--- Issue composed with the help of DeepSeek
Source: vercel/next.js