Stack trace with fs/promises and async..await
Asked Answered
N

3

10

I have a problem that is similar to this one and described in this issue. There are no stack traces in errors that come from fs/promises when used with async..await, and no way to determine where the error happened.

Given the app:

foo.js

const { bar } = require('./bar');

async function foo() {
  await bar();
}


(async () => {
  await foo();
})().catch(console.error);

bar.js

const fs = require('fs/promises');

exports.bar = async function bar() {
  await fs.readFile('some');
}

When it runs like node foo.js, this results in an error with no stack trace:

[Error: ENOENT: no such file or directory, open '...\some'] {
  errno: -4058,
  code: 'ENOENT',
  syscall: 'open',
  path: '...\\some'
}

Where error.stack contains 'ENOENT: no such file or directory, open '...\some'.

When it runs like node --stack_trace_limit=100 -r trace foo.js with trace package, like it's suggested in linked sources, this results in this stack trace:

stack trace

Notice that internal entries are grayed and can be filtered out.

This node --stack_trace_limit=100 -r trace -r clarify foo.js with trace and clarify packages results in this output:

Error: ENOENT: no such file or directory, open '...\some'
    at ...\foo.js:8:2
    at Object.<anonymous> (...\foo.js:10:3) {
  errno: -4058,
  code: 'ENOENT',
  syscall: 'open',
  path: '...\\some',
  [Symbol(originalCallSite)]: [],
  [Symbol(mutatedCallSite)]: [ CallSite {}, CallSite {} ]
}

The problem is that bar.js isn't mentioned in all of the outputs.

My intention is to provide clean error output with no internal entries and the exact location where the error occurs, i.e. line number in bar.js.

What are possible solutions to this problem?

Neils answered 31/3, 2022 at 20:35 Comment(4)
Out of curiosity, besides needing to write more code, what is your aversion to using try/catch?Calie
Writing more code is the exact reason. The last time I had this problem I had to rethrow fs errors in a dozen places I didn't plan, the existing catches didn't give enough info without proper stack traceNeils
Gotcha. I was wondering if it had to do with any performance overhead with try/catch. As far as I know if there is any overhead it’s negligible.Calie
The overhead is supposed to exist, both stack and try-catch aren't free, this is the reason fs didn't originally implement it, but it was unmeasurable indeed when I tested it with promisesNeils
C
2

One possible solution is to use Node.js >= v21.2.0. Node.js release v21.2.0 included a commit from PR 49849 that adds stack traces to node:fs/promises.

ES modules and top level await.

package.json

"type": "module"

bar.js

import { readFile } from 'node:fs/promises'

export async function bar() {
  await readFile('some')
}

foo.js

import { bar } from './bar.js'

await bar()

Now run node foo.js to generate the stack trace:

node:internal/fs/promises:638
  return new FileHandle(await PromisePrototypeThen(
                        ^

Error: ENOENT: no such file or directory, open 'some'
    at async open (node:internal/fs/promises:638:25)
    at async readFile (node:internal/fs/promises:1251:14)
    at async bar (file:///Users/user/async-stack-trace/bar.js:4:5)
    at async file:///Users/user/async-stack-trace/foo.js:4:1 {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: 'some'
}

Node.js v22.1.0

ES modules with async function (no top level await).

foo.js

import { bar } from './bar.js'

async function main() {
  await bar()
}

main()

Which after running node foo.js results in a similar stack trace:

node:internal/fs/promises:638
  return new FileHandle(await PromisePrototypeThen(
                        ^

Error: ENOENT: no such file or directory, open 'some'
    at async open (node:internal/fs/promises:638:25)
    at async readFile (node:internal/fs/promises:1251:14)
    at async bar (file:///Users/user/async-stack-trace/bar.js:4:5)
    at async main (file:///Users/user/async-stack-trace/foo.js:4:3) {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: 'some'
}

Node.js v22.1.0

CommonJS

package.json

"type": "commonjs"

bar.js

const { readFile } = require('node:fs/promises')

exports.bar = async function bar() {
  await readFile('some')
}

foo.js

const { bar } = require('./bar.js')

async function main() {
  await bar()
}

main()

After running node foo.js:

node:internal/fs/promises:638
  return new FileHandle(await PromisePrototypeThen(
                        ^

Error: ENOENT: no such file or directory, open 'some'
    at async open (node:internal/fs/promises:638:25)
    at async readFile (node:internal/fs/promises:1251:14)
    at async bar (/Users/user/async-stack-trace/src/bar.js:4:3)
    at async main (/Users/user/async-stack-trace/src/foo.js:4:3) {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: 'some'
}

Node.js v22.1.0
Calie answered 6/5 at 1:5 Comment(1)
Thanks, great news, didn't notice the change in time because I used 20 LTSNeils
I
4

So close :)

Edit: based on fs/promise issue, it's a known issue, and could be worked-around on demand, as suggested below.

This node --stack_trace_limit=100 -r trace -r clarify foo.js with trace and clarify packages results in this output:

Error: ENOENT: no such file or directory, open '...\some' at ...\foo.js:8:2 at Object. (...\foo.js:10:3) {
errno: -4058,
code: 'ENOENT',
syscall: 'open',
path: '...\some',
[Symbol(originalCallSite)]: [],
[Symbol(mutatedCallSite)]: [ CallSite {}, CallSite {} ]
}

The problem is that bar.js isn't mentioned in all of the outputs.

In addition for using trace/clarify, do try/catch directly on fs.readFile call (bar.js)

const fs = require('fs/promises');

exports.bar = async () => {
    try {
        await fs.readFile('some');
    } catch (e) {
        console.error(e);
    }
}
$ node --stack_trace_limit=100 -r trace -r clarify foo.js
Error: ENOENT: no such file or directory, open 'some'
    at emitInitNative (node:internal/async_hooks:205:43)
    at emitInitNative (node:internal/async_hooks:205:43)
    at emitInitScript (node:internal/async_hooks:495:3)
    at promiseInitHook (node:internal/async_hooks:325:3)
    at promiseInitHookWithDestroyTracking (node:internal/async_hooks:329:3)
    at Object.readFile (node:internal/fs/promises:786:24)

    at exports.bar (/home/lz/code/stackover/bar.js:6:18)

    at foo (/home/lz/code/stackover/foo.js:4:11)
    at /home/lz/code/stackover/foo.js:9:15
    at Object.<anonymous> (/home/lz/code/stackover/foo.js:13:3)
    at Module._compile (node:internal/modules/cjs/loader:1101:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:17:47 {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: 'some'
}

You can also try sync read file.

Isocline answered 5/4, 2022 at 2:19 Comment(3)
The problem is that this requires to use try..catch in every place where fs is used, something I'd prefer to avoid at all cost, or to use my own wrappers for fs which is very awkward. The problem with this snippet is that it doesn't rethrow an error like another answer suggests. The caller is supposed to be aware of the error. sync methods are not an option at this point because I wend with fs/promises for non-blocking FS ops, but now I see the problem with using it as a rule of thumb.Neils
@EstusFlask i feel like wrapper is the way, could be awkward, but there are some pros to it - you can switch/experiment fs implementation/lib from a single place in your code. from what I read here, it is a known bug, unresolved for performance reasons which might be relevant in your case. loosing some details from the error for a gain in performance is a small price to pay.Isocline
I'd expect that there's an existing wrapper library that does that, but the ones I know like fs-extra seem to fall back to native implementation. Any way, thanks for the answerNeils
C
4

Since the root cause of your problem is that fs/promises Errors have no stack by design, why simply not throwing your own when you need stack details?

More than this I can suggest to avoid using the catch method of promises and to use only the async/await construct.

foo.js

const { bar } = require('./bar');

async function foo() {
  await bar();
}

(async function main() {
  try {
    await foo();
  } catch(e) {
    console.error(e);
  }
})();

bar.js

const fs = require('fs/promises');

exports.bar = async function bar() {
  try{
    await fs.readFile('some');
  } catch(e) {
    throw new Error(e.message);
  }
};

This gives following result:

Error: ENOENT: no such file or directory, open 'some'
    at bar (/home/me/test/bar.js:7:11)
    at async foo (/home/me/test/foo.js:4:3)
    at async main (/home/me/test/foo.js:9:5)

Probably you neither need to use trace and/or clarify, but of course you are still free to use them to alter the stack trace at your needs.

Cichocki answered 10/4, 2022 at 21:10 Comment(2)
I hoped for a more general solution. The problem is that this requires to use try..catch in every place where fs is used, something I'd prefer to avoid at all cost, or to use my own wrappers for fs which is very awkward. Any way, I'll likely use new Error as a quick fix and will restrain myself from using fs/promises in non-critical parts in future. .catch was used as a catch-all for AIIFE, I don't think it affects the way error handling works here.Neils
I know @EstusFlask : very bad! ATM I think there is no better solution than writing your own wrappers for the functions you need to trace. As a catch-all mechanism I use an async function strictly fully contained in a try/catch block for all entry points of my programs (the main function, event handles, etc...).Cichocki
C
2

One possible solution is to use Node.js >= v21.2.0. Node.js release v21.2.0 included a commit from PR 49849 that adds stack traces to node:fs/promises.

ES modules and top level await.

package.json

"type": "module"

bar.js

import { readFile } from 'node:fs/promises'

export async function bar() {
  await readFile('some')
}

foo.js

import { bar } from './bar.js'

await bar()

Now run node foo.js to generate the stack trace:

node:internal/fs/promises:638
  return new FileHandle(await PromisePrototypeThen(
                        ^

Error: ENOENT: no such file or directory, open 'some'
    at async open (node:internal/fs/promises:638:25)
    at async readFile (node:internal/fs/promises:1251:14)
    at async bar (file:///Users/user/async-stack-trace/bar.js:4:5)
    at async file:///Users/user/async-stack-trace/foo.js:4:1 {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: 'some'
}

Node.js v22.1.0

ES modules with async function (no top level await).

foo.js

import { bar } from './bar.js'

async function main() {
  await bar()
}

main()

Which after running node foo.js results in a similar stack trace:

node:internal/fs/promises:638
  return new FileHandle(await PromisePrototypeThen(
                        ^

Error: ENOENT: no such file or directory, open 'some'
    at async open (node:internal/fs/promises:638:25)
    at async readFile (node:internal/fs/promises:1251:14)
    at async bar (file:///Users/user/async-stack-trace/bar.js:4:5)
    at async main (file:///Users/user/async-stack-trace/foo.js:4:3) {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: 'some'
}

Node.js v22.1.0

CommonJS

package.json

"type": "commonjs"

bar.js

const { readFile } = require('node:fs/promises')

exports.bar = async function bar() {
  await readFile('some')
}

foo.js

const { bar } = require('./bar.js')

async function main() {
  await bar()
}

main()

After running node foo.js:

node:internal/fs/promises:638
  return new FileHandle(await PromisePrototypeThen(
                        ^

Error: ENOENT: no such file or directory, open 'some'
    at async open (node:internal/fs/promises:638:25)
    at async readFile (node:internal/fs/promises:1251:14)
    at async bar (/Users/user/async-stack-trace/src/bar.js:4:3)
    at async main (/Users/user/async-stack-trace/src/foo.js:4:3) {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: 'some'
}

Node.js v22.1.0
Calie answered 6/5 at 1:5 Comment(1)
Thanks, great news, didn't notice the change in time because I used 20 LTSNeils

© 2022 - 2024 — McMap. All rights reserved.