Skip to content

error printer: show file/line for JSC parser SyntaxErrors with no stack - #36634

Open
robobun wants to merge 4 commits into
mainfrom
farm/21e500e1/fix-5192-syntaxerror-location
Open

robobun wants to merge 4 commits into
mainfrom
farm/21e500e1/fix-5192-syntaxerror-location

Conversation

@robobun

@robobun robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #5192.

Problem

A SyntaxError raised by JavaScriptCore's own parser (strict-mode with, delete x on an unqualified identifier, invalid regex literals, etc.) during module load printed only the bare message with no file, line, or code frame:

SyntaxError: 'with' statements are not valid in strict mode.

Bun v1.4.0 (Linux x64)

Bun's own parser errors already showed location; this is specific to errors JSC catches after Bun's transpiler accepted the source.

Cause

These errors are created before any JS runs, so the ErrorInstance has no stack. addErrorInfo() records the line and sourceURL on the instance's C++ fields, but materializeErrorInfoIfNeeded() only publishes them as .line/.sourceURL JS properties when a stack trace exists. fromErrorInstance() read those JS properties, found nothing, and produced zero frames, so the code-frame printer never ran.

Fix

fromErrorInstance() now reads err->sourceURL()/err->line() from the C++ fields directly and synthesizes a frame when no stack is available.

JSC never records the parser-error column (it is always 0). A source-map lookup at column 0 resolves to the printer's start-of-line mapping, which points at the tail of the previous source line when the offending statement is indented, so the reported line was off by one. Both the new path and the existing <parse> frame in formatStackTrace() now query past end-of-line so the lookup lands on the last mapping of the generated line, which reliably resolves to the correct source line; the column is reported as 1 since the true column is unknown.

After

1 | "use strict";
2 | export const x = 1;
3 | function foo() {
4 |   with ({ a: 1 }) {
    ^
SyntaxError: 'with' statements are not valid in strict mode.
      at /tmp/t/module.mjs:4:1

Bun v1.4.0 (Linux x64)

Verification

$ USE_SYSTEM_BUN=1 bun test test/regression/issue/05192.test.ts   # 0 pass, 7 fail
$ bun bd test test/regression/issue/05192.test.ts                 # 7 pass, 0 fail

Covers: direct entry, import of .mjs/.ts, await import() re-thrown via console.error, require() of CJS, and the indented-statement off-by-one.


[review] gate passed · iteration 0 · 4 files touched

fails on main (without fix)
ASAN without fix: 7 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/regression/issue/05192.test.ts"
bun test v1.4.0 (02618d2db)

test/regression/issue/05192.test.ts:
35 | 
36 | test.concurrent("JSC parse SyntaxError in the entry module prints file and line", async () => {
37 |   const { stderr, exitCode } = await run({ "entry.mjs": withInStrict }, "entry.mjs");
38 |   expect(stderr).toContain("SyntaxError: 'with' statements are not valid in strict mode.");
39 |   // File path with a line number must appear somewhere in the output.
40 |   expect(stderr).toMatch(/entry\.mjs:\d+/);
                      ^
error: expect(received).toMatch(expected)

Expected substring or pattern: /entry\.mjs:\d+/
Received: "SyntaxError: 'with' statements are not valid in strict mode.\n\nBun v1.4.0-debug+02618d2db (Linux x64)\n"

      at <anonymous> (/workspace/bun/test/regression/issue/05192.test.ts:40:18)
(fail) JSC parse SyntaxError in the entry module prints file and line [680.38ms]
64 |       "entry.ts": `import "./module.ts";`,
65 |     },
66 |     "entry.ts",
67 |   );
68 |   expect(stderr).toContain("SyntaxErr
... (truncated)

release without fix: 7 FAILED
bun test v1.4.0-canary.1 (1498d7b77)

test/regression/issue/05192.test.ts:
35 | 
36 | test.concurrent("JSC parse SyntaxError in the entry module prints file and line", async () => {
37 |   const { stderr, exitCode } = await run({ "entry.mjs": withInStrict }, "entry.mjs");
38 |   expect(stderr).toContain("SyntaxError: 'with' statements are not valid in strict mode.");
39 |   // File path with a line number must appear somewhere in the output.
40 |   expect(stderr).toMatch(/entry\.mjs:\d+/);
                      ^
error: expect(received).toMatch(expected)

Expected substring or pattern: /entry\.mjs:\d+/
Received: "SyntaxError: 'with' statements are not valid in strict mode.\n\nBun v1.4.0-canary.1+1498d7b77 (Linux x64)\n"

      at <anonymous> (/workspace/bun/test/regression/issue/05192.test.ts:40:18)
(fail) JSC parse SyntaxError in the entry module prints file and line [42.44ms]
50 |       "entry.ts": `import "./module.mjs";`,
51 |     },
52 |     "entry.ts",
53 |   );
54 |   expect(stderr).toContain("SyntaxError: 'with' statements are not valid in strict mode.");
55 |   expect(stderr).toMatch(/module\.mjs:\d+/);
                      ^
error: expect(received).toMatc
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/regression/issue/05192.test.ts"
bun test v1.4.0 (02618d2db)

test/regression/issue/05192.test.ts:
(pass) JSC parse SyntaxError in the entry module prints file and line [371.65ms]
(pass) JSC parse SyntaxError in an imported ESM module prints file and line [373.75ms]
(pass) JSC parse SyntaxError in an imported .ts module prints file and line [374.02ms]
(pass) JSC parse SyntaxError: 'delete x' in strict mode prints file and line [367.20ms]
(pass) JSC parse SyntaxError from dynamic import() prints file and line when re-thrown [348.55ms]
(pass) JSC parse SyntaxError in a require()'d CJS module prints the offending line [463.51ms]
(pass) JSC parse SyntaxError reported line points at the offending statement, not the line before [461.33ms]

 7 pass
 0 fail
 26 expect() calls
Ran 7 tests across 1 file. [5.15s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     02618d2db7
  features     baseline

22 deps, 108 codegen, 1171 objects in 2974ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1234] fetch picohttpparser
[picohttpparser] up to date
[2/1234] gen JSBuffer.lut.h
Generating /workspace/bun/build/release/codegen/JSBuffer.lut.h from /workspace/bun/src/jsc/bindings/JSBuffer.cpp
[3/1234] gen ProcessBindingBuffer.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingBuffer.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingBuffer.cpp
[4/1234] gen ProcessBindingConstants.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingConstants.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingConstants.cpp
[5/1234] fetch zlib
[zlib] up to date
[6/1234] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[7/1234] fetch tinycc
[tinycc] up to date
[8/1234] gen ProcessBindingFs.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingFs.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingFs.cpp
... (truncated)
diff hotspot
src/jsc/bindings/FormatStackTraceForJS.cpp |  11 +--
 src/jsc/bindings/ZigException.cpp          |  18 ++++
 src/jsc/bindings/headers-handwritten.h     |  11 +++
 test/regression/issue/05192.test.ts        | 153 +++++++++++++++++++++++++++++
 4 files changed, 185 insertions(+), 8 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                        reads  edits  tests
src/jsc/bindings/FormatStackTraceForJS.cpp      5      2      0
src/jsc/bindings/ZigException.cpp               5      8      0
src/jsc/bindings/headers-handwritten.h          3      3      0
test/regression/issue/05192.test.ts             0      3      0

A SyntaxError raised by JavaScriptCore's own parser (strict-mode 'with',
'delete x', invalid regex literals, etc.) during module load has no JS
stack frames: the error is created before any JS runs. addErrorInfo()
records line and sourceURL on the ErrorInstance's C++ fields, but
materializeErrorInfoIfNeeded() only publishes them as .line/.sourceURL
JS properties when a stack trace exists, so the error printer found
nothing and emitted a bare 'SyntaxError: ...' with no file, line, or
code frame.

fromErrorInstance() now reads err->sourceURL()/err->line() from the C++
fields directly and synthesizes a frame when no stack is available.

JSC never records the parser-error column (column is always 0), and a
source-map lookup at column 0 resolves to bun's own start-of-line
mapping for the previous source line when the offending statement is
indented. Both the new path and the existing <parse> frame in
formatStackTrace() now query past end-of-line so the lookup lands on
the last mapping of the generated line, which reliably resolves to the
correct source line.

Fixes #5192
@robobun

robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: reproduced, fix verified locally, review threads resolved.

bun bd test test/regression/issue/05192.test.ts passes 7/7; USE_SYSTEM_BUN=1 fails 7/7.

CI (build 86698): 05192.test.ts passes on every lane. Remaining reds are unrelated to this diff:

  • bun-install-registry.test.ts on Windows aarch64 (package hoisting; reported to main-break triage)
  • 36577.test.ts on Windows x64 (pre-existing)
  • the rest are batch-only flakes that passed when retried alone (cpu-prof, node-module-module, request-clone-leak, inspect-error-leak, webview, mongodb, etc.)

None of these touch the error-printer surface this PR changes. Ready for a maintainer.

PR: #36634

@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d86c0f43-39e8-46eb-9ebd-3f62185ffdd5

📥 Commits

Reviewing files that changed from the base of the PR and between 5f7e62d and 02618d2.

📒 Files selected for processing (4)
  • src/jsc/bindings/FormatStackTraceForJS.cpp
  • src/jsc/bindings/ZigException.cpp
  • src/jsc/bindings/headers-handwritten.h
  • test/regression/issue/05192.test.ts

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the claude label Aug 1, 2026
@robobun

robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 2:56 AM PT - Aug 1st, 2026

❌ @robobun, your commit 02618d2 has 2 failures in Build #86698 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36634

That installs a local version of the PR into your bun-36634 executable, so you can run:

bun-36634 --bun

Comment thread src/jsc/bindings/FormatStackTraceForJS.cpp Outdated
Comment thread src/jsc/bindings/ZigException.cpp Outdated
Comment thread src/jsc/bindings/ZigException.cpp Outdated
Comment thread src/jsc/bindings/ZigException.cpp Outdated
Comment thread src/jsc/bindings/headers-handwritten.h Outdated
Comment thread test/regression/issue/05192.test.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No bugs found. Traced the INT32_MAX column trick through Mapping::find_index_from_generated (src/sourcemap/Mapping.rs:156) — it's a lower-bound binary search, so "past end-of-line" resolves to the last mapping on the generated line as intended, and remap_stack_frame_positions calls resolve_source_mapping with NoSourceContents so the sentinel column never reaches the ParseUrlResultHint::All hint. Deferring to a human because this is a cross-language change to shared error-formatting paths (fromErrorInstance, formatStackTrace) that every printed error flows through, and the past-EOL remap approach is worth a maintainer's eyes.

Extended reasoning...

Overview

The PR touches three C++ files in src/jsc/bindings/ and adds a regression test. It fixes #5192: JSC-parser SyntaxErrors (strict-mode with, delete x, etc.) printed with no file/line because the error has no JS stack and materializeErrorInfoIfNeeded() never published .line/.sourceURL to the JS side. The fix reads err->sourceURL()/err->line() from the C++ fields directly in fromErrorInstance() and synthesizes a single frame; a new shared helper Bun__remapParseErrorFrame handles the source-map lookup for both this new path and the existing <parse> frame in formatStackTrace(). Because JSC's addErrorInfo() records column 0 for parser errors, the helper queries at column INT32_MAX so the lookup lands on the last mapping of the generated line, then reports column 1.

Security risks

None. This is diagnostic-output formatting only; no untrusted-input parsing, allocation sizing, or privilege boundaries are affected.

Level of scrutiny

Moderate-to-high. The change is small in line count but sits in shared error-formatting code that every printed error flows through (fromErrorInstance, formatStackTrace), spans the C++/Rust FFI boundary, and relies on a specific implementation detail of the source-map lookup (that find_index_from_generated is a "last mapping ≤ target" binary search returning None only when no mapping exists on the target line). I traced that through src/sourcemap/Mapping.rs:156-185 and it holds; remap_stack_frame_positions in VirtualMachine.rs calls resolve_source_mapping with NoSourceContents, so the INT32_MAX column never reaches the ParseUrlResultHint::All { column, .. } hint path. The is_invalid() gate in the Rust batch loop requires all three position fields to be −1, and the helper sets line/column to non-negative values, so remapping proceeds.

Other factors

Test coverage is thorough (7 concurrent subprocess tests covering entry vs. static import vs. dynamic import vs. require(), .mjs/.ts/.cjs, and the indented-statement off-by-one that motivated the INT32_MAX trick). The comment-cop bot flags on long comments were addressed in follow-up commits (53e3fe6c). My earlier nit about test-file placement was answered — the author confirmed #5192 did work in bun 1.0.1 and later regressed, so test/regression/issue/ is correct. I'm deferring rather than approving because the past-EOL remap heuristic and the new early-return branch in fromErrorInstance (which now short-circuits the JS-property fallback whenever the C++ sourceURL() is non-empty) are the kind of behavior change a maintainer should confirm doesn't affect adjacent error shapes.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No bugs found, but this touches JSC error-printer C++ with a source-map heuristic that warrants a human look — specifically the INT32_MAX column trick in Bun__remapParseErrorFrame, which relies on the Rust remapper clamping to the last mapping on the generated line, and which also changes the existing <parse>-frame path in formatStackTrace() (previously looked up column 0).

What was reviewed:

  • fromErrorInstance new branch: frames_ptr[0] write matches the sibling JS-property fallback (same deref()/frames_len=1 pattern, no frames_cap check needed).
  • err->sourceURL()/line()/column() are plain C++ field reads, no JS re-entry or exception scope needed.
  • Test file placement (my earlier nit) was answered — #5192 is a genuine regression per the 1.0.1 output; thread resolved.
  • Tests cover entry/import/require/dynamic-import and the indented off-by-one; test.concurrent, hermetic, drain both pipes.
Extended reasoning...

Overview

The PR fixes #5192: JSC-parser SyntaxErrors (strict-mode with, delete x, etc.) printed with no file/line because the ErrorInstance has no JS stack and materializeErrorInfoIfNeeded() never published .line/.sourceURL as JS properties. The fix reads err->sourceURL()/err->line() directly from the C++ fields in fromErrorInstance() and synthesizes a single frame. It also extracts a shared Bun__remapParseErrorFrame helper in headers-handwritten.h and rewires the existing <parse>-frame path in FormatStackTraceForJS.cpp to use it.

Security risks

None identified. The change is confined to error-message formatting; no untrusted-input parsing, no allocation driven by external sizes, no auth/crypto.

Level of scrutiny

Medium. This is C++ JSC-bindings code on the error-reporting path — user-visible and historically fiddly — but the blast radius is diagnostic output, not runtime semantics. Two things push it above auto-approve:

  1. The INT32_MAX column heuristic. Bun__remapParseErrorFrame sets column_zero_based = INT32_MAX when JSC's parser column is 0, so the source-map lookup lands on the last mapping of the generated line rather than the previous line's tail. This depends on how VirtualMachine::remap_stack_frame_positions / the underlying source-map lookup handles a column past end-of-line. The PR asserts it "reliably resolves to the correct source line" and the two exact-line tests (mod.cjs:2, module.mjs:3) back it, but a reviewer familiar with the source-map implementation should confirm this holds generally (e.g. multi-mapping lines, transpiled TS where the printer emits several source-line mappings on one generated line, or files with no source map at all).
  2. It modifies an existing code path. The <parse> frame in formatStackTrace() previously remapped at column 0; it now goes through the same INT32_MAX helper. That's a behavioral change to an already-shipping path (used by require() of a bad CJS module, per the tests), not purely additive.

Other factors

  • Tests are solid: 7 concurrent subprocess tests covering entry vs. imported .mjs/.ts, delete x, dynamic import() re-thrown via console.error, require() of CJS, and the indented-statement off-by-one. Two assert exact line numbers. Verified fail-on-main / pass-on-PR under both ASAN debug and release.
  • The comment-cop bot flags were addressed (commits "trim comment blocks to single lines" and "extract Bun__remapParseErrorFrame helper; trim comments").
  • My earlier nit about test/regression/issue/ placement was answered: the author showed bun 1.0.1 did print a location, so it is a true regression.
  • The one CI failure (test/regression/issue/36577.test.ts on Windows x64) is unrelated to this diff.
  • The new frames_ptr[0] write follows the exact pattern of the pre-existing JS-property fallback immediately below it (same source_url.deref(), same frames_len = 1), so no new memory-ownership concern relative to what's already there.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect line and column numbers when SyntaxError happens

2 participants