From 693150703a54a8afc5dd0f27cd4a4122748defb2 Mon Sep 17 00:00:00 2001 From: vedchaudhari Date: Thu, 15 Oct 2026 15:21:02 +0530 Subject: [PATCH] fs: restore stack traces of fs/promises readFile and writeFile errors Errors from readFile(), writeFile() and appendFile() have had no stack frames since they started using a single thread pool round trip, as the stack was captured from a C++ callback with no JS frames on it. Capture it after the await instead, like the rest of fs/promises. Refs: https://github.com/nodejs/node/pull/65327 Refs: https://github.com/nodejs/node/pull/65489 Assisted-by: claude:opus-5.5 Signed-off-by: vedchaudhari --- lib/internal/fs/promises.js | 28 +++++++++++++++++++++------- test/parallel/test-fs-promises.js | 25 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index c21b1e5740fc..f7b5c1f23c5c 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -2204,8 +2204,14 @@ async function writeFile(path, data, options) { if (!flush && !isCustomIterable(data) && data.byteLength <= kWriteFileMaxChunkSize) { path = getValidatedPath(path); - await writeFileInOneRoundTrip(path, stringToFlags(flag, 'options.flag'), - parseFileMode(options.mode, 'mode', 0o666), data); + const flagsNumber = stringToFlags(flag, 'options.flag'); + const mode = parseFileMode(options.mode, 'mode', 0o666); + try { + await writeFileInOneRoundTrip(path, flagsNumber, mode, data); + } catch (err) { + ErrorCaptureStackTrace(err); + throw err; + } checkAborted(options.signal); // An abort during the write still wins. return; } @@ -2222,6 +2228,8 @@ async function writeFile(path, data, options) { /** * Open + write + close as one thread pool round trip. + * The caller is responsible for capturing the stack trace of a rejection, + * as no JS frames are on the stack when the job completes. * @param {string|Buffer} path Validated path * @param {number} flagsNumber * @param {number} mode @@ -2233,7 +2241,6 @@ function writeFileInOneRoundTrip(path, flagsNumber, mode, data) { const job = new binding.WriteFileJob(path, flagsNumber, mode, data); job.ondone = (err) => { if (err != null) { - ErrorCaptureStackTrace(err, writeFileInOneRoundTrip); reject(err); } else { resolve(); @@ -2241,7 +2248,6 @@ function writeFileInOneRoundTrip(path, flagsNumber, mode, data) { }; const accessError = job.run(path); if (accessError !== undefined) { - ErrorCaptureStackTrace(accessError, writeFileInOneRoundTrip); reject(accessError); } }); @@ -2287,7 +2293,15 @@ async function readFile(path, options) { // up to one chunk; larger files come back as an open fd + size and are // read by readFileHandle() as before. path = getValidatedPath(path); - const { 0: buffer, 1: fd, 2: size } = await readFileInOneRoundTrip(path, stringToFlags(flag)); + const flagsNumber = stringToFlags(flag); + let result; + try { + result = await readFileInOneRoundTrip(path, flagsNumber); + } catch (err) { + ErrorCaptureStackTrace(err); + throw err; + } + const { 0: buffer, 1: fd, 2: size } = result; if (fd === -1) { checkAborted(options.signal); // An abort during the read still wins. return options.encoding ? buffer.toString(options.encoding) : buffer; @@ -2301,6 +2315,8 @@ async function readFile(path, options) { } /** + * The caller is responsible for capturing the stack trace of a rejection, + * as no JS frames are on the stack when the job completes. * @param {string|Buffer} path Validated path * @param {number} flagsNumber * @returns {Promise<[Buffer|undefined, number, number|undefined]>} [buffer, -1] or [undefined, fd, size] @@ -2311,7 +2327,6 @@ function readFileInOneRoundTrip(path, flagsNumber) { job.ondone = (err, buffer, fd, size, closeErr) => { const error = err ?? closeErr; if (error != null) { - ErrorCaptureStackTrace(error, readFileInOneRoundTrip); reject(error); } else { resolve([buffer, fd, size]); @@ -2319,7 +2334,6 @@ function readFileInOneRoundTrip(path, flagsNumber) { }; const accessError = job.run(path); if (accessError !== undefined) { - ErrorCaptureStackTrace(accessError, readFileInOneRoundTrip); reject(accessError); } }); diff --git a/test/parallel/test-fs-promises.js b/test/parallel/test-fs-promises.js index 07f01187e89b..6ddc4cd3cb36 100644 --- a/test/parallel/test-fs-promises.js +++ b/test/parallel/test-fs-promises.js @@ -9,6 +9,7 @@ const fs = require('fs'); const fsPromises = fs.promises; const { access, + appendFile, chmod, chown, copyFile, @@ -62,6 +63,30 @@ assert.strictEqual( } ).then(common.mustCall()); + assert.rejects( + readFile('this file does not exist'), + { + code: 'ENOENT', + stack: /at async assert\.rejects/ + } + ).then(common.mustCall()); + + assert.rejects( + writeFile(path.join('this dir does not exist', 'file'), 'data'), + { + code: 'ENOENT', + stack: /at async assert\.rejects/ + } + ).then(common.mustCall()); + + assert.rejects( + appendFile(path.join('this dir does not exist', 'file'), 'data'), + { + code: 'ENOENT', + stack: /at async assert\.rejects/ + } + ).then(common.mustCall()); + assert.rejects( access(__filename, 8), {