Skip to content

import() calls record no source location, so errors point at an earlier line or the function header #1752

Description

@yspbwx2010

The stack frame of a function that calls import() does not point at the import() call. It points at whatever source location the function recorded last before the call: an earlier call on a previous line, the start of the enclosing expression statement, or the function itself. At the top level of a module that last case gives 1:1.

I reproduced it with the qjs binary built from the v0.17.0 release tarball. dep.js does not need to exist in any of the examples.

The most direct case is an error thrown by code that import() runs synchronously. Here it is a getter on the options object:

repro-getter.js:

function main() {
    console.log("loading");
    return import("./dep.js", { get with() { throw new Error("boom"); } });
}

main().catch(e => console.log(e.message + "\n" + e.stack));
$ qjs repro-getter.js
loading
boom
    at get with (repro-getter.js:3:56)
    at main (repro-getter.js:2:13)
    at <anonymous> (repro-getter.js:6:1)

The frame for main points at log on line 2. The import() call is at 3:12.

The same location shows up when import() rejects without running any user code, for example because the options argument is invalid. The TypeError has no stack yet. It gets one when the caller's await rethrows it, and the await right after the import() call maps to the same place:

repro.js:

async function main() {
    console.log("loading");
    const mod = await import("./dep.js", { with: 1 });
}

main().catch(e => console.log(e.message + "\n" + e.stack));
$ qjs repro.js
loading
options.with must be an object
    at main (repro.js:2:13)

The call is at 3:23.

repro-toplevel.mjs:

// top-level await in a module
const mod = await import("./dep.js", { with: 1 });
$ qjs repro-toplevel.mjs
TypeError: options.with must be an object
    at <anonymous> (repro-toplevel.mjs:1:1)

Line 1 is the comment. The call is at 2:19.

You cannot see this with a missing module and the stock qjs loader. That error is created in a job with no JS frames, so it gets an empty stack. An embedder that leaves the stack unset in that case, so that the await fills it in, gets the same wrong location.

The cause is in js_parse_postfix_expr. The TOK_IMPORT case emits OP_import with no OP_source_loc before it:

https://github.com/quickjs-ng/quickjs/blob/19dbe8524c1a7357d268cb586ee315616984fef8/quickjs.c#L27320-L27362

Most other operations that can throw record a location, such as calls, new, identifier references, binary operators, throw and iterator close. Expression statements record where they start. So the pc of the import() call, and of an await directly after it, resolves to the previous entry in the pc2line table. The fix is to save the position of the import keyword and emit it right before OP_import, the same way #1473 did for the for-of iterator close. With that change the examples above report 3:12, 3:23 and 2:19. I have a patch with a regression test and can open a PR.

I reproduced this on v0.17.0. The parser code involved is unchanged on master at 19dbe85. This change does not help errors that are created later in a job and never have JS frames, for example from an asynchronous loader. That is a separate problem, related to the async stack trace work in #1248.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions