Skip to content

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

Description

@debugmcpdev

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:

  1. 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.
  2. 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.

File B:

function helper(base) {
  const total = base + 1;
  return total;
}
console.log('started');
setTimeout(() => console.log('late total', helper(41)), 3000);

The function-breakpoint form — set_breakpoint {function: "work"} then start_debugging (no stopOnEntry) on:

function work(base) {
  const total = base + 1;
  return total;
}
setTimeout(() => console.log('late total', work(41)), 2000);

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.

Activity

  1. debugmcpdev commented on Oct 6, 2026

    @debugmcpdev
    CollaboratorAuthor

    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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions