const fs = require('fs');
async function read() {
return fs.promises.readFile('non-exist');
}
read()
.then(() => console.log('done'))
.catch(err => {
console.log(err);
})
gives:
➜ d2e2027b node app.js
[Error: ENOENT: no such file or directory, open 'non-exist'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: 'non-exist'
}
➜ d2e2027b
The stack is missing. If I use fs.readFileSync
instead, it shows the stack as expected.
➜ d2e2027b node app.js
Error: ENOENT: no such file or directory, open 'non-exist'
at Object.openSync (node:fs:582:3)
at Object.readFileSync (node:fs:450:35)
at read (/private/tmp/d2e2027b/app.js:4:13)
at Object.<anonymous> (/private/tmp/d2e2027b/app.js:8:1)
As a super-ugly-workaround, I can put try/catch and throw a new error in case of ENOENT but I'm sure there is a better solution out there.
read()
.then(() => console.log('done'))
.catch(err => {
if (err.code === 'ENOENT') throw new Error(`ENOENT: no such file or directory, open '${err.path}'`);
console.log(err);
})
(I tried node v12, v14, v16 - same same)
Nodejs has several modules which throw errors with useless stack
properties; in my opinion this is a bug, but it has existed since the beginning of nodejs and likely cannot be changed at this point for fear of backwards compatibility (EDIT: I take this back; the stack
property is non-standard and devs should know not to rely on its structure; nodejs really should make a change to throw more meaningful errors).
I've wrapped all such functions that I use in nodejs, modifying them to throw good errors instead. Such wrappers can be created with this function:
let formatErr = (err, stack) => {
// The new stack is the original Error's message, followed by
// all the stacktrace lines (Omit the first line in the stack,
// which will simply be "Error")
err.stack = [ err.message, ...stack.split('\n').slice(1) ].join('\n');
return err;
};
let traceableErrs = fnWithUntraceableErrs => {
return function(...args) {
let stack = (new Error('')).stack;
try {
let result = fnWithUntraceableErrs(...args);
// Handle Promises that resolve to bad Errors
let isCatchable = true
&& result != null // Intentional loose comparison
&& result.catch != null // Intentional loose comparison
&& (result.catch instanceof Function);
return isCatchable
? result.catch(err => { throw formatErr(err, stack); })
: result;
} catch(err) {
// Handle synchronously thrown bad Errors
throw formatErr(err, stack);
}
};
}
This wrapper handles simple functions, functions which returns promises, and async functions. The basic premise is to initially generate a stack when the wrapper function is called; this stack will have the caller chain that lead to the call of the wrapper function. Now if errors are thrown (either sync or async) we catch the error, set its stack
property to the useful value, and throw it once again; "catch-and-release" if you will.
Here's what I see in my terminal using this approach:
> let readFile = traceableErrs(require('fs').promises.readFile);
> (async () => await readFile('C:/nonexistent.txt'))().catch(console.log);
Promise { <pending> }
> ENOENT: no such file or directory, open 'C:\nonexistent.txt'
at repl:5:18
at repl:1:20
at repl:1:48
at Script.runInThisContext (vm.js:120:20)
at REPLServer.defaultEval (repl.js:433:29)
at bound (domain.js:426:14)
at REPLServer.runBound [as eval] (domain.js:439:12)
at REPLServer.onLine (repl.js:760:10)
at REPLServer.emit (events.js:327:22)
at REPLServer.EventEmitter.emit (domain.js:482:12) {
errno: -4058,
code: 'ENOENT',
syscall: 'open',
path: 'C:\\nonexistent.txt'
}
If you want to modify the whole fs.promises
suite to throw good errors you can do:
let fs = { ...require('fs').promises };
for (let k in fs) fs[k] = traceableErrs(fs[k]);
(async () => {
// Now all `fs` functions throw stackful errors
await fs.readFile(...);
await fs.writeFile(...);
})();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With