fs.promises.readFile ENOENT 错误中没有堆栈

Posted

技术标签:

【中文标题】fs.promises.readFile ENOENT 错误中没有堆栈【英文标题】:no stack in fs.promises.readFile ENOENT error 【发布时间】:2021-09-02 10:07:15 【问题描述】:
const fs = require('fs');

async function read() 
  return fs.promises.readFile('non-exist');


read()
  .then(() => console.log('done'))
  .catch(err => 
    console.log(err);
  )

给予:

➜  d2e2027b node app.js
[Error: ENOENT: no such file or directory, open 'non-exist'] 
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: 'non-exist'

➜  d2e2027b

堆栈丢失。如果我改用fs.readFileSync,它会按预期显示堆栈。

➜  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)

作为一个超级丑陋的解决方法,我可以在 ENOENT 的情况下使用 try/catch 并抛出一个新错误,但我确信那里有更好的解决方案。

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);
  )

(我尝试了节点 v12、v14、v16 - 相同)

【问题讨论】:

【参考方案1】:

Nodejs 有几个模块会抛出无用 stack 属性的错误;在我看来这是一个错误,但它从 nodejs 开始就存在,并且由于担心向后兼容性而可能无法更改(编辑:我收回这个;stack 属性是非标准的,开发人员应该知道不要依赖它的结构;nodejs 真的应该做出改变以抛出更有意义的错误)。

我已经包装了我在 nodejs 中使用的所有此类函数,并对其进行了修改以抛出好的错误。可以使用此函数创建此类包装器:

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);
      
    
    
  ;
  

这个包装器处理简单函数、返回承诺的函数和异步函数。基本前提是在调用包装函数时初步生成堆栈;此堆栈将具有导致调用包装函数的调用者链。现在,如果抛出错误(同步或异步),我们会捕获错误,将其stack 属性设置为有用的值,然后再次抛出它;如果你愿意的话,“抓到就放”。

这是我使用这种方法在终端中看到的:

> 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'

如果你想修改整个 fs.promises 套件以抛出好的错误,你可以这样做:

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(...);
)();

【讨论】:

感谢@Gershy,这是一种有趣的方法,您基本上是将堆栈从new Error('') 分配给当前错误。我快速测试了它,似乎消息消失了。编辑:我将其更改为以下throw Object.assign(err, stack: new Error(err.message).stack );,消息又回来了。我不能说我喜欢它,但也许目前没有更好的选择。 @David 唯一的问题是,如果您在 catch 回调中调用 new Error,堆栈的用处将大大降低 - 我正在快速编辑;让我知道它是否适合您...

以上是关于fs.promises.readFile ENOENT 错误中没有堆栈的主要内容,如果未能解决你的问题,请参考以下文章