Node.js Stream piping 에러 해결
Understanding and fixing common stream piping errors in Node.js including EPIPE, ERR_STREAM_WRITE_AFTER_END, and backpressure issues.
Node.js Stream piping 에러 해결
Introduction
Node.js streams are powerful for handling large amounts of data, but they come with their own set of error handling challenges. Improperly handled stream pipes can lead to memory leaks, data corruption, and unhandled exceptions. This post covers the most common stream piping errors and their solutions.
Environment
node --version
# v20.11.0
# Testing with large files
ls -lh large-dataset.csv
# -rw-r--r-- 1 user user 2.1G Jul 10 10:30 large-dataset.csvProblem
Here is a typical file copy operation that fails with stream errors:
const fs = require('fs');
const readStream = fs.createReadStream('large-dataset.csv');
const writeStream = fs.createWriteStream('copy-dataset.csv');
readStream.pipe(writeStream);After processing about 500MB, the process crashes with:
Error: read EPIPE
at afterReadDispatch [as original] (node:internal/stream_base_commons:98:11)
at onStreamRead (node:internal/stream_base_commons:241:5)
Error [ERR_STREAM_WRITE_AFTER_END]: write after end
at WriteStream.write (node:internal/streams/writable:493:15)
at ReadStream.ondata (node:internal/streams/readable:758:22)Analysis
Stream piping errors occur for several reasons:
- The destination stream closes before the source finishes (EPIPE)
- Writing to a stream that has already ended (ERR_STREAM_WRITE_AFTER_END)
- Backpressure not handled properly - writing faster than the destination can handle
- Error events not caught on either stream
The pipe lifecycle looks like this:
Readable → pipe() → Writable
↓
backpressure signal
↓
pauses reading
↓
drains write buffer
↓
resumes readingIf any step in this lifecycle fails without proper error handling, the process crashes.
Solution
Solution 1: Proper error handling with pipe
const fs = require('fs');
const readStream = fs.createReadStream('large-dataset.csv');
const writeStream = fs.createWriteStream('copy-dataset.csv');
readStream.on('error', (err) => {
console.error('Read error:', err.message);
writeStream.destroy(err);
});
writeStream.on('error', (err) => {
console.error('Write error:', err.message);
readStream.destroy(err);
});
writeStream.on('finish', () => {
console.log('Copy complete');
});
readStream.pipe(writeStream);Solution 2: Using pipeline() (recommended for Node.js 10+)
const { pipeline } = require('stream');
const fs = require('fs');
const readStream = fs.createReadStream('large-dataset.csv');
const writeStream = fs.createWriteStream('copy-dataset.csv');
pipeline(
readStream,
writeStream,
(err) => {
if (err) {
console.error('Pipeline failed:', err.message);
} else {
console.log('Pipeline succeeded');
}
}
);Solution 3: Promisified pipeline
const { pipeline } = require('stream');
const { promisify } = require('util');
const fs = require('fs');
const pipelineAsync = promisify(pipeline);
async function copyFile(src, dest) {
const readStream = fs.createReadStream(src);
const writeStream = fs.createWriteStream(dest);
try {
await pipelineAsync(readStream, writeStream);
console.log('File copied successfully');
} catch (err) {
console.error('Copy failed:', err.message);
}
}
copyFile('large-dataset.csv', 'copy-dataset.csv');Solution 4: Transform streams with error handling
const { Transform, pipeline } = require('stream');
const csvParser = new Transform({
transform(chunk, encoding, callback) {
try {
const lines = chunk.toString().split('\n');
const parsed = lines
.filter(line => line.trim())
.map(line => {
const [name, email] = line.split(',');
return JSON.stringify({ name: name?.trim(), email: email?.trim() });
})
.join('\n');
callback(null, parsed + '\n');
} catch (err) {
callback(err);
}
}
});
pipeline(
fs.createReadStream('contacts.csv'),
csvParser,
fs.createWriteStream('contacts.jsonl'),
(err) => {
if (err) console.error('Transform failed:', err);
else console.log('Transform complete');
}
);Solution 5: Handling backpressure explicitly
const readable = getLargeDataStream();
const writable = fs.createWriteStream('output.bin');
readable.on('data', (chunk) => {
const canContinue = writable.write(chunk);
if (!canContinue) {
readable.pause();
writable.once('drain', () => {
readable.resume();
});
}
});
readable.on('end', () => {
writable.end();
});Lessons Learned
- Always use
pipeline()instead of.pipe()- It handles cleanup and error propagation automatically - Never ignore stream errors - Use
.on('error', handler)on every stream - Destroy streams on error - Call
.destroy(err)to clean up resources - Test with large files - Stream issues often only appear with data larger than the internal buffer (typically 16KB)
- Monitor memory usage - Improper backpressure handling causes memory to grow unbounded
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.