You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
JavaScript: the entry stop lands inside the first-declared function when the program calls it — stopOnEntry stops late, and a function breakpoint on it skips its first call #858
For a JavaScript file whose first declaration is a function that the program later calls, the launch's entry stop does not land at the program's first statement. It lands inside that function, the first time it is called — after everything the program did before that call — and is still reported as reason: "entry".
Two things follow from it:
stopOnEntry: true stops late, in the wrong place, with the program already run. If the function is first called three seconds in, start_debugging blocks for those three seconds and then answers paused / entry with the top frame inside the function.
A function breakpoint on that function skips its first call. A launch with a pending JavaScript function breakpoint forces an entry stop to bind it (js-debug function breakpoints via the CDP proxy (revives #282 by another route) #295) and auto-continues that stop when stopOnEntry was not requested. Here the "entry" stop is the first call of the function, so the breakpoint is armed after the call has already been entered, the stop is resumed as an unrequested entry stop, and the program runs on. If that was the only call, the program runs to completion with the breakpoint listed verified: true and never hit.
Reproduce
Windows 11, Node v24.14.1, js-debug 1.112.0, main at f3cbde3 (also on the build of #857). Every launch is start_debugging {scriptPath, dapLaunchArgs: {stopOnEntry: true}} followed by get_stack_trace and get_output.
file
first lines
start_debugging answered after
top frame at the "entry" stop
stdout before the stop
A
statement, then a function, then a timer that calls it
3.0 s
<anonymous> line 1
—
B
function helper, then console.log('started'), then a 3 s timer that calls helper
6.0 s
helper line 2
started
C
a comment, then B
5.9 s
helper line 3
started
D
'use strict';, then B
6.0 s
helper line 3
started
E
B, but the timer does not call helper (it is never called)
2.6 s
<anonymous> line 5
—
F
B, with helper also called from the first statement
2.5 s
helper line 2
—
(3 s of each launch is start-up plus the launch's own hold; the extra 3 s in B–D is the timer.) A and E are correct. B, C, D and F are not: the stop is inside the function. What decides it is whether the first-declared function is called, and when.
runs to completion: late total 42, exit code 0, no stop; list_breakpoints reports the function breakpoint verified: true.
What the trace shows (function-breakpoint form)
DAP_TRACE=1, child connection, and the proxy log:
24.218 [Worker] Forcing stopOnEntry=true in the js-debug launch config (pending CDP function breakpoints, issue #295)
26.178 child -> attach {"__pendingTargetId": "...", "continueOnAttach": true}
26.272 [CdpFnBpBridge] attached to CDP proxy
... no stop at program start; the program runs and schedules its timer ...
28.228 child <- stopped {"reason": "entry", "description": "Paused on breakpoint", "hitBreakpointIds": [], "threadId": 0}
28.229 [CdpFnBpBridge] Debugger.paused recorded (hits=1, ...)
28.234 [CdpFnBpBridge] armed 'work' as 7:1 (adapterId 1000000, bound .../latefn.js ...)
28.235 [Worker] DAP event: stopped reason=entry
28.237 child -> continue (auto-continue: stopOnEntry was not requested)
28.240 output "late total 42"
28.241 terminated
The stop js-debug reports as entry arrives 2.0 s after the program started — when the timer calls work — with a CDP pause that hit one breakpoint (hits=1, "Paused on breakpoint"): js-debug's entry breakpoint has resolved to the first break position in the file by source order, which is inside the first function's body, rather than to the first statement the program executes.
Why it matters
The entry stop is treated as certain to come: a stopOnEntry launch waits for it (up to launchReadyCeilingMs, 30 s). With a first-declared function that is called late, the launch blocks until then; the caller sees a slow launch and a program that has already produced output and side effects by the time it "stops on entry".
The function-breakpoint path depends on the entry stop being at program start; when it is not, the breakpoint's first hit is lost without any signal — the worst kind of miss, since list_breakpoints says verified: true.
Not yet known
Whether the entry breakpoint's location is chosen by js-debug or by V8's resolution of a line-0 breakpoint (a breakpoint requested on a line that only declares a function slides into the function body), and whether VS Code shows the same stop for file B. A js-debug trace (trace: true in adapterLaunchConfig) of file B would settle it. If it is upstream behaviour, mcp-debugger could still recognise the case: the entry stop's top frame is not the module's top-level frame.
Settled the "not yet known" part with a js-debug trace (adapterLaunchConfig.trace) of file B and the js-debug 1.112.0 sources:
js-debug's NodeLauncher.createLifecycle().initialized sends Debugger.setBreakpointByUrl { urlRegex: <program>(?:entryBreakpoint){0}, lineNumber: 0, columnNumber: 0 } for stopOnEntry; the trace shows Debugger.breakpointResolved at line 1 col 16 — inside helper — and the Debugger.paused 3 s later with callFrames[0].functionName === 'helper', reported as DAP entry. V8 resolves a (0,0) breakpoint with FindClosestSharedFunctionInfoFromPosition: among the functions intersecting [0, first top-level break position] it picks the one whose first breakable position is nearest in source order. So: js-debug placement + V8 resolution; VS Code gets the same stop.
A facet the report misses: the entry breakpoint is never removed. With helper called twice (1.5 s and 3 s), the second call stops again, reported as another entry.
mcp-debugger's own --inspect-brk=9229 append for stopOnEntry was inert: js-debug strips --inspect-brk out of runtimeArgs into stopOnEntry (resolveParams in the vendored bundle), so there is no Break-on-start pause to rescue; attachSimplePort mode would give one but drops the bootloader (child-process debugging, the exit-code shim). Node's --inspect-brk pauses on the first executed statement via callAndPauseOnStart, which is why it does not have this problem.
Filed upstream as microsoft/vscode-js-debug#2430. On our side (PR coming): the CDP bridge reports an entry stop whose paused frame is the function it just armed (matched by [[FunctionLocation]]) as that function breakpoint — the silent first-call miss — and a stopOnEntry launch that lands inside a function carries an explanation in the stop's text. A real fix (planting a hidden breakpoint on the first top-level statement, found with the vendored acorn, and swallowing js-debug's own late entry) is possible but not in this round.
What happens
For a JavaScript file whose first declaration is a function that the program later calls, the launch's entry stop does not land at the program's first statement. It lands inside that function, the first time it is called — after everything the program did before that call — and is still reported as
reason: "entry".Two things follow from it:
stopOnEntry: truestops late, in the wrong place, with the program already run. If the function is first called three seconds in,start_debuggingblocks for those three seconds and then answerspaused/entrywith the top frame inside the function.stopOnEntrywas not requested. Here the "entry" stop is the first call of the function, so the breakpoint is armed after the call has already been entered, the stop is resumed as an unrequested entry stop, and the program runs on. If that was the only call, the program runs to completion with the breakpoint listedverified: trueand never hit.Reproduce
Windows 11, Node v24.14.1, js-debug 1.112.0, main at f3cbde3 (also on the build of #857). Every launch is
start_debugging {scriptPath, dapLaunchArgs: {stopOnEntry: true}}followed byget_stack_traceandget_output.start_debugginganswered after<anonymous>line 1helper, thenconsole.log('started'), then a 3 s timer that callshelperhelperline 2startedhelperline 3started'use strict';, then Bhelperline 3startedhelper(it is never called)<anonymous>line 5helperalso called from the first statementhelperline 2(3 s of each launch is start-up plus the launch's own hold; the extra 3 s in B–D is the timer.) A and E are correct. B, C, D and F are not: the stop is inside the function. What decides it is whether the first-declared function is called, and when.
File B:
The function-breakpoint form —
set_breakpoint {function: "work"}thenstart_debugging(nostopOnEntry) on:runs to completion:
late total 42, exit code 0, no stop;list_breakpointsreports the function breakpointverified: true.What the trace shows (function-breakpoint form)
DAP_TRACE=1, child connection, and the proxy log:The stop js-debug reports as
entryarrives 2.0 s after the program started — when the timer callswork— with a CDP pause that hit one breakpoint (hits=1, "Paused on breakpoint"): js-debug's entry breakpoint has resolved to the first break position in the file by source order, which is inside the first function's body, rather than to the first statement the program executes.Why it matters
stopOnEntrylaunch waits for it (up tolaunchReadyCeilingMs, 30 s). With a first-declared function that is called late, the launch blocks until then; the caller sees a slow launch and a program that has already produced output and side effects by the time it "stops on entry".list_breakpointssaysverified: true.Not yet known
Whether the entry breakpoint's location is chosen by js-debug or by V8's resolution of a line-0 breakpoint (a breakpoint requested on a line that only declares a function slides into the function body), and whether VS Code shows the same stop for file B. A js-debug trace (
trace: trueinadapterLaunchConfig) of file B would settle it. If it is upstream behaviour, mcp-debugger could still recognise the case: theentrystop's top frame is not the module's top-level frame.