Node.js中fs.createReadStream的ENOENT错误无法捕获如何解决
问题背景
我在使用Node.js流管道开展文件处理相关开发时,计划测试各类错误场景,第一个测试用例为打开不存在的文件,但测试后发现无法捕获该场景下抛出的错误,fs模块并不会触发预期的error事件,该问题在fs.unlink接口上也同样存在。
复现代码
const fName = 'C:/temp/fff.txt'; const outStream = fs.createWriteStream(fName + '.enc'); console.log(`-- 1111 ${fName} `); fs.createReadStream(fName) .pipe(outStream) .on('open', () => { console.log(`ON OPEN ${fName}`); }) .on('error', (err) => { console.log(`ON ERROR ${fName}; an error happened: ${err.message}`); }) .on('close', () => { console.log(`ON CLOSE ${fName}`); }) .on('end', () => { console.log(`ON END ${fName}`); }); console.log(`-- 2222 ${fName} `);
运行现象
- 目标文件存在时,运行输出:
-- 1111 C:/temp/fff.txt -- 2222 C:/temp/fff.txt ... ON OPEN C:/temp/fff.txt ON CLOSE C:/temp/fff.txt
- 目标文件不存在时,运行直接抛出未捕获异常:
-- 1111 C:/temp/fff.txt -- 2222 C:/temp/fff.txt ... ON OPEN C:/temp/fff.txt 2022-06-13T19:28:48.379Z; error; uncaughtException: ENOENT: no such file or directory, open 'C:\temp\ffff.txt' Error: ENOENT: no such file or directory, open 'C:\temp\ffff.txt'; C:\Users\p>
有开发者提出猜想:是否存在error事件在stream.on('error', ...)绑定逻辑生效前就已经被触发的可能性?
问题根因
这个猜想完全正确,问题核心出在两个点:
- 事件绑定时机错误:
fs.createReadStream()执行后会立刻在同步阶段发起文件打开操作,代码中是在调用.pipe(outStream)之后才绑定error监听,此时读取流打开不存在文件触发的ENOENT错误已经发出,没有对应监听器的情况下会直接作为未捕获异常抛出。 - 原生
pipe()方法不会自动转发上游读取流的错误到下游返回的流上,代码中绑定的error监听实际是加在写入流outStream上,根本接收不到读取流抛出的错误。
正确处理方案
- 方案1:提前为所有流单独绑定错误监听,再执行pipe操作
先分别创建读取流、写入流,给两个流都挂载好error事件监听,再调用pipe方法,出错时记得手动销毁未关闭的流避免资源泄漏:const fName = 'C:/temp/fff.txt'; const readStream = fs.createReadStream(fName); const outStream = fs.createWriteStream(fName + '.enc'); // 提前绑定错误监听 readStream.on('error', (err) => { console.log(`读取文件失败: ${err.message}`); outStream.destroy(); }); outStream.on('error', (err) => { console.log(`写入文件失败: ${err.message}`); readStream.destroy(); }); // 绑定完监听再执行pipe readStream.pipe(outStream) .on('open', () => { console.log(`ON OPEN ${fName}`); }) .on('close', () => { console.log(`ON CLOSE ${fName}`); }) .on('end', () => { console.log(`ON END ${fName}`); }); - 方案2:使用官方推荐的
pipeline方法替代原生pipe(Node.js 10+支持)stream.pipeline会自动转发上下游所有流的错误,在处理完成或出错时自动销毁所有关联流,从根源避免资源泄漏和错误捕获不到的问题,支持回调和Promise两种调用方式:const fs = require('fs'); const { pipeline } = require('stream/promises'); async function processFile() { const fName = 'C:/temp/fff.txt'; try { await pipeline( fs.createReadStream(fName), fs.createWriteStream(fName + '.enc') ); console.log('文件处理完成'); } catch (err) { console.log(`文件处理失败: ${err.message}`); } } processFile(); - 针对fs.unlink的同类问题:fs.unlink是标准的异步回调接口,错误会通过回调函数的第一个参数返回,使用时需要在回调中判断处理错误;如果是Promise版本的fs.unlink,直接通过
try/catch捕获即可。
内容的提问来源于stack exchange,提问作者VladP
相关产品推荐
相关产品推荐

