Error Events Don't Inherently Stop Streams In Node.js

Perhaps another basic Stream post for some of you, but I'm still trying to flesh out my own mental model of how streams work in Node.js. One thing that wasn't immediately clear to me was how Streams interacted with errors. Part of the problem is that I didn't know how to reconcile .pipe() behavior with the core error-handling behavior; which is to say that error events don't inherently stop (or end or close) streams in Node.js.

When I first started looking into Streams, I had assumed that when a Node.js stream encounters an error, it stops working. After all, how can a stream recover from an error? Unfortunately, this assumption was wrong. To demonstrate, we can emit errors in both Readable and Writable streams and continue to use them without issue.

First, let's look at a Readable stream. In the following code, I'm going to emit an "error" event whenever I go to read source data into the underlying stream buffer. I'm also going to log "error", "readable", and "end" events so we can see what executes:

// Include module references.

var events = require( "events" );

var stream = require( "stream" );

var util = require( "util" );

var chalk = require( "chalk" );

// ---------------------------------------------------------- //

// ---------------------------------------------------------- //

// I am a reabable stream in object-mode.

function Outie() {

stream.Readable.call(

this,

{

objectMode: true

}

);

this._source = [ "What", "it", "be", "like?" ];

}

util.inherits( Outie, stream.Readable );

Outie.prototype._read = function( sizeIsIgnored ) {

while ( this._source.length ) {

// Emit an error every time we go to push data into the internal buffer.

// --

// NOTE: You would never want to do this - I am only doing this to

// demonstrate the interplay between Readable streams and error events.

this.emit( "error", new Error( "StreamError" ) );

if ( ! this.push( this._source.shift() ) ) {

break;

}

}

if ( ! this._source.length ) {

this.push( null );

}

};

// ---------------------------------------------------------- //

// ---------------------------------------------------------- //

// Create an instance of our readable stream.

var readable = new Outie();

// Make sure that we bind an error-event handler. If we don't, then the EventEmitter

// will raise an exception (and crash the process) when our stream emits an error event.

readable.on(

"error",

function handleError( error ) {

console.log( chalk.red( "Readable error:", error.message ) );

}

);

readable.on(

"readable",

function handleReadable() {

var data = null;

while ( null !== ( data = this.read() ) ) {

console.log( chalk.cyan( "Data:", data ) );

}

}

);

readable.on(

"end",

function handleEnd() {

console.log( chalk.yellow( "End" ) );

}

);

When we run this code through Node.js, we get the following terminal output:

As you can see, I emit four errors as the source data is read into the underlying buffer. But, even with these so-called errors, the "readable" event is still triggered properly, I can still read-in the stream data in chunks, and the "end" event fires once all the data has been consumed.

The same is true with Writable streams. In the following demo, I'm going emit an error whenever I go to write data to the stream. This time, however, I'm not only emitting the "error" event, I'm also invoking the write-callback with an error object, which will, in turn, emit another error event:

// Include module references.

var events = require( "events" );

var stream = require( "stream" );

var util = require( "util" );

var chalk = require( "chalk" );

// ---------------------------------------------------------- //

// ---------------------------------------------------------- //

// I am a writable stream in object-mode.

function Innie() {

stream.Writable.call(

this,

{

objectMode: true

}

);

this._buffer = "";

this.on(

"finish",

function handleFinish() {

this.emit( "debug", this._buffer );

}

);

}

util.inherits( Innie, stream.Writable );

Innie.prototype._write = function( chunk, encoding, writeDone ) {

this._buffer += ( chunk + " " );

// Emit an error every time we go to write data into the running buffer.

// --

// NOTE: You would never want to do this - I am only doing this to

// demonstrate the interplay between Readable streams and error events.

this.emit( "error", new Error( "StreamError" ) );

// Alternatively, we can also pass an error as the first argument to our

// callback to signify that the chunk was not consumed properly (this will

// cause another error event to be emitted).

writeDone( new Error( "CallbackError" ) );

};

// ---------------------------------------------------------- //

// ---------------------------------------------------------- //

// Create an instance of our writable stream.

var writable = new Innie();

// Make sure that we bind an error-event handler. If we don't, then the EventEmitter

// will raise an exception (and crash the process) when our stream emits an error event.

writable.on(

"error",

function handleError( error ) {

console.log( chalk.red( "Writable error:", error.message ) );

}

);

writable.on(

"finish",

function handleFinish() {

console.log( chalk.yellow( "Finish" ) );

}

);

writable.on(

"debug",

function handleDebug( buffer ) {

console.log( chalk.cyan( "Debug:", buffer ) );

}

);

writable.write( "What" );

writable.write( "it" );

writable.write( "be" );

writable.end( "like?" );

When we run this code through Node.js, we get the following terminal output:

Here, you can see that each call to .write() ended up triggered two different error events: the one that we emit explicitly and the one implicitly triggered by the callback. And yet, even after all these errors, we still build up the encapsulated buffer, we still finish the stream, and we're still able to output the resultant value.

All in all, the error events seem to have no bearing on how the individual Readable and Writable streams function. Of course, this isn't meant to be a covering statement - if you don't bind to the "error" event, Node.js will crash; and, if you're using .pipe(), Node.js attempts to .unpipe() the piped connection upon error. And, if you're consuming a sub-classed Stream, you have to adhere to that stream's documented behavior. But, when we're talking about core streams, it seems like the "error" event isn't too special.

I am the co-founder and lead engineer at InVision App, Inc — the world's leading prototyping,
collaboration & workflow platform. I also rock out in JavaScript and ColdFusion 24x7 and I dream about
promise resolving asynchronously.