Error: Import failed points to a line that creates an error. Opening that line tells you how the application reports a failure. To find what actually failed, follow the attached cause to the parser, then inspect the input that reached it.
Download the complete JavaScript stack trace lab, unzip it, and enter read-javascript-stack-trace. It uses local files and Node’s built-in modules; no installation step or network request is involved. The example is constructed for this tutorial. Its import step reads and parses records without saving them anywhere. All commands below were run with Node.js 22.22.1; use Node.js 22 or later.
The three source files give you a small path to investigate:
| File | Work it performs | Evidence to inspect |
|---|---|---|
run.mjs |
Chooses an input and awaits the import | Input filename, result, exit status |
importer.mjs |
Reads the file, calls the parser, wraps failures | The try block and the caught cause |
parser.mjs |
Passes text to JSON.parse |
The parser call and the supplied text |
Run the failing input and keep both stacks
node run.mjs with-cause
This command is supposed to fail with exit status 1. The output below combines its stdout and stderr; only the absolute directory prefix has been replaced with file:///lab/. File names, line numbers, columns, and the error text are from the recorded run.
Input: records.bad.json
Error: Import failed
at importRecords (file:///lab/importer.mjs:10:19)
at async file:///lab/run.mjs:14:19
Caused by:
SyntaxError: Expected double-quoted property name in JSON at position 29 (line 2 column 28)
at JSON.parse (<anonymous>)
at parseRecords (file:///lab/parser.mjs:2:15)
at importRecords (file:///lab/importer.mjs:7:12)
at async file:///lab/run.mjs:14:19
run.mjs prints error.stack and error.cause.stack separately. The first stack belongs to the wrapper; the second belongs to the SyntaxError it caught. Keep that distinction when copying an error report. A logger that saves only the outer message has discarded most of the useful evidence here.
In importer.mjs:10:19, the final two numbers identify a source line and column. In the parser’s message, “line 2 column 28” refers to the JSON input. Those are two different coordinate systems. Match each location to the file or value it describes, using the same source version that produced the trace.
Open the construction line, then read the catch branch
Here is the entire importer.mjs from the download:
import { readFile } from 'node:fs/promises';
import { parseRecords } from './parser.mjs';
export async function importRecords(input, { preserveCause = true } = {}) {
try {
const text = await readFile(input, 'utf8');
return parseRecords(text);
} catch (cause) {
const options = preserveCause ? { cause } : undefined;
const error = new Error('Import failed', options);
throw error;
}
}
Line 10 creates the wrapper. Line 11 throws that existing object. The outer stack starts at line 10 because Node records the location of new Error(). Throwing the same object later does not move that captured location. The Node.js 22 error documentation describes this behavior.
The try block also matters: both readFile and parseRecords can fail, and both failures receive the same outer message. “Import failed” alone cannot tell you which branch failed. For example, the lab’s missing-file test gets an ENOENT cause rather than a SyntaxError.
Follow the cause to the parser and its actual input
The cause stack names JSON.parse first. Continue to the first frame in the code you can inspect: parser.mjs:2:15.
export function parseRecords(text) {
return JSON.parse(text);
}
Then follow text back to readFile(input, 'utf8'). run.mjs selected records.bad.json, whose complete contents are:
[
{"id": "tea", "count": 2,}
]
The comma after 2 leaves the parser expecting another property name. It encounters } at the reported input location instead. You now have an input, a specific operation, and a reproducible failure. The wrapper’s construction line was the entry point into the investigation; changing its message would leave this JSON unchanged.
new Error(message, { cause }) keeps the caught value accessible on the new error. Node added that option in 16.9.0; it is documented for the 22.22.1 used here. The value can be anything, so general error-handling code should check its type before reading .stack. This lab’s printer checks instanceof Error. See Node’s error.cause reference.
Remove the cause and see what information disappears
node run.mjs without-cause
It also exits with status 1. The same input still fails, but the output now ends with:
Error: Import failed
at importRecords (file:///lab/importer.mjs:10:19)
at async file:///lab/run.mjs:14:19
Cause: unavailable
This mode passes no cause option to new Error. The wrapper no longer carries the parser error. Reading its stack cannot recover the discarded input location or distinguish this failure from the missing-file branch. You would need another observation, such as a local reproduction with the original error preserved.
Keeping cause improves what you can investigate. It does not repair the input: with-cause still fails, as the first command showed.
Use async frames as clues, then fill the gaps from code
Both recorded stacks include at async .../run.mjs:14:19, the awaiting caller. By the time parsing fails, readFile has already resolved; its work is absent from this trace even though its result supplied the parser.
V8’s async stack documentation describes reconstruction around await and selected Promise operations, including Promise.all() and Promise.any(). Those frames cover selected async relationships. They do not provide a complete history of callbacks, timers, or I/O. A missing frame is not proof that the corresponding work never ran.
The SyntaxError wording and available frames can vary with Node/V8 versions. Use the displayed locations to open the matching source; do not treat this transcript as a universal stack format. The lab tests check useful error properties and application frames without matching the whole stack string.
Test the retry hypothesis against the input
Consider the claim “This is a temporary network problem; retrying should fix it.” That is a teaching hypothesis to test, not a quote from an AI system or a real incident.
The trace gives you a SyntaxError at JSON.parse. The source shows that the parser received bytes from a local file. There is no network operation in this lab, and three attempts with the unchanged bad bytes all reject. The proposed explanation has no supporting evidence here.
Now run the corrected fixture, which removes only the trailing comma:
node run.mjs fixed
It exits with status 0 and prints:
Input: records.fixed.json
Parsed records: [{"id":"tea","count":2}]
Then run the regression checks:
node --test importer.test.mjs
All nine tests pass. They cover the preserved parser cause, the lossy wrapper, repeated failure on unchanged input, successful corrected input, a missing-file cause, construction versus later throwing, and the three CLI modes. The two failing demonstration commands are expected failures; they are not failing regression tests.
This establishes the explanation for these local bytes. In a real importer, continue to the producer of the invalid text before deciding why it was invalid. The lab checks JSON syntax and the example result; it does not validate a record schema or persist an import.
Read the surrounding code in Yenpo, verify in your terminal
Open the extracted folder in Yenpo and use the code-reading guide to locate importer.mjs, parser.mjs, and the chosen input. Start at the reported lines, then read the whole catch branch and the caller that supplies input.
With a configured model, Yenpo’s AI Reader can help inspect that path. A focused request would be:
Trace this Import failed error from its construction line through cause to the input passed to JSON.parse. Cite the files and lines. Distinguish what the stack shows, what the source establishes, and what still needs a terminal run. Check whether retrying can change the bytes in this example.
Run the commands in your terminal and compare the output with the explanation.