Writable Streams
A Writable stream is a destination you push data into — a file being written, an HTTP response, process.stdout, a socket. You call write() to send chunks and end() to finish. The subtlety lives in write()'s return value, which is how a writable signals backpressure — and ignoring it is the single most common stream bug.
Basic writing
import { createWriteStream } from 'node:fs'
const out = createWriteStream('output.txt')
out.write('first line\n')
out.write('second line\n')
out.end('last line\n') // optional final chunk, then closes
out.on('finish', () => console.log('all data flushed'))
out.on('error', (err) => console.error(err))write() returns a boolean — this is backpressure
Respecting backpressure manually
function writeMany(stream, items) {
return new Promise((resolve, reject) => {
let i = 0
write()
function write() {
let ok = true
while (i < items.length && ok) {
const chunk = items[i++]
if (i === items.length) {
stream.end(chunk) // last one
} else {
ok = stream.write(chunk) // false ⇒ buffer full
}
}
if (i < items.length) {
// paused by backpressure — resume when drained
stream.once('drain', write)
}
}
stream.on('finish', resolve)
stream.on('error', reject)
})
}Why pipeline beats manual writing
The manual backpressure loop above is correct but error-prone. When you're moving data from a readable, let pipeline own the whole dance:
import { createReadStream, createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
// Backpressure, errors, and cleanup all handled:
await pipeline(
createReadStream('source.dat'),
createWriteStream('dest.dat'),
)The key events
Event | Meaning |
|---|---|
| Buffer drained below |
| All data flushed; |
| Stream and underlying resource closed |
| A write or the underlying resource failed |
| A readable started/stopped piping into this writable |
cork and uncork: batching small writes
Many tiny write() calls each incur overhead. cork() buffers writes in memory; uncork() (or the next tick) flushes them as one larger operation — useful when emitting many small chunks back-to-back:
socket.cork() for (const part of headerParts) socket.write(part) process.nextTick(() => socket.uncork()) // flush batched writes together
Object mode and custom writables
import { Writable } from 'node:stream'
// A sink that consumes objects and persists them:
const dbWriter = new Writable({
objectMode: true,
write(record, _encoding, callback) {
saveToDatabase(record)
.then(() => callback()) // signal "ready for next chunk"
.catch(callback) // pass error → emits 'error'
},
})
dbWriter.write({ id: 1 })
dbWriter.end()Quick reference
Goal | Approach |
|---|---|
Write then close |
|
Move data from a readable |
|
Respect backpressure | Stop on |
Know when flushed | Wait for |
Batch small writes |
|
Custom sink |
|