Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

no stack in fs.promises.readFile ENOENT error

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)

like image 664
David Avatar asked Sep 08 '25 04:09

David


1 Answers

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(...);
})();
like image 83
Gershom Maes Avatar answered Sep 09 '25 17:09

Gershom Maes