After seeing Jonathan Ong’s explanation while I was at CampJS 2014, I was aware there were a number of deficiencies with the ever-popular express framework. After spending longer in fighting the complexity battle I understood more and more of the justification behind it, however, in pushing for a less well-known framework I was - quite reasonably - asked to justify it’s use.
Koa vs Express
It’s come up a few times: “Why are you using this hipster framework with iojs?!”. Given the massive amount of churn in the JS sphere, this is a reasonable response. Creating yet another microframework around your project is apparently a right of passage in some circles and I realise a strong tendency to be different for the sake of being different - the differentiation of the hipster - is something which is a real burden upon anyone else not inclined to self-expression-via-framework-authorship having to learn and support all this code.
It’s my contention is that the use of Koa and ES6 is not bourne out of this, but rather an attempt real-world deficiencies of javascript on the server-side. It offers significant technical advantages and its use and the use of more recent releases of node/iojs - despite being less familiar to front-end devs - are defensible. Moreover should seem actually more familiar to back-end devs.
What the author of express says:
He says use Koa. Why care? He wrote both.
The problems in working with Node:
In writing this, I assume You know what the pyramid of death is, and know about promises and the async library and friends.
Promises:
- Promises are an improvement over callbacks, but have lots of problems:
They don’t solve the async uncaught exception problem:
try { function returnAPromise(){ return new Promise(function(resolve, reject){ setTimeout(function(){ throw "some error"; resolve('This worked'); }, 500); }); } returnAPromise() .then(function(data){ console.log('success', data); // This will never reach here }, function(err){ console.error('error', err); //This will never fire }); } catch(e){ console.log('error caught', e); //This will never catch the error }
They can swallow errors if they’re not used properly:
function returnAPromise(){ return new Promise(function(resolve, reject){ reject("some error") }); } returnAPromise() .then(function(data){ console.log(data); });
NodeJS’s the try-catch model is fundamentally unable to deal with async code.
- They are seldom used in the Node Community (cf comments of Rod Vagg et al, though this may change with the greater adoption of es6)
- Maybe I don’t understand them, but as far as I’m aware, unless they are chained they are virtually worthless offering nothing more than an elaborate callback, but in being chained they don’t lend them selves to error handling in-flow either, nor are they particularly easy to add arguments to functions partway in the chain.
- Tried passing multiple parameters to the middle of a promise chain? It’s annoying.
The async libary:
Async was revolution for node at the time. However, it’s still relatively painful to work with and has the same problems as promises, it doesn’t catch async errors, it’s even harder to do a sane waterfall or series flow in a readable fashion and it’s very easy to swallow errors.
Now, what Koa fixes:
Consider:
try{
// Do this:
const requestObject = yield request('http://some-api.com/'); //An async request
//Now this:
yield anArrayOfPromises; //A parallel set of requests, for example
}
catch(e){
console.error(e); //this will catch exceptions
}
- Superior flow control: series or parallel asyncronous functions as required.
- Significantly simplified async constructs;
- Use try/catch where you want to, even asyncronously.
- Significantly improved stream handling
- Greater elegance in the application code - no monkey patching the HTTP core.
Counterarguments:
“But everyone uses express”: This is a bad argument.
More detailed shouting noises:
Writing enterprise Node code is a server-side activity and should not be considered a transplantation of front-end development. The language is the same and the problem of asyncronous execution is the same, but the patterns, problems and ecosystem are so significantly different they may as well be thought of as two different activities. UI developer’s need not be be excessively concerned about memory leaks, floating file-descriptors, SQL injection attacks, ACID properties or distributed systems problems. Client-side development occurs within a specialised space where the developer is extraordinarily constrained by the limitations of the client browser and system. As such, front-end patterns and tools are not necessarily best carried across verbatim just because of their use in client-side development.
The es6 features here are being co-opted into making NodeJS capable of handling async functionality in a way which should be reasonably familiar to C# developers and, anecdotally, they seem comfortable when exposed to it. More so, in fact than with the front-end paradigms of promise chains and similar. Therefore, I would suggest that it is worth using insofar as it mimics a paradigm server-side devs are already familiar with and with less trade-offs.
Everyone working with Node has worked with Express in some capacity and it would make sense that it’s the go-to defacto webserver for APIs. Instead, I suggest looking at it from the perspective of someone seeing the language for the first time and objectively asking “what tools suit the problem best in this domain?”.
What have generators and es6 got to do with it?
Others smarter than me will be able to provide a better of the how it works, but essentially Koa uses the co flow control system which in turn takes a generator and executes the functions that come out of it and passes the return values back in. The user supplies the generator in their code, and gets handed back the values from the application, thereby allowing the flow-control from within the generator to appear as almost syncronous. This technique has been around for a while, and was discussed at a detailed technical level by Andrey Sidorov at MelbJS a while back.
Es6 is an established standard with the majority of the better ideas already implemented in the various browser engines. Using es6 features such as block-scoped variables within node already has significant precedent.
Is it bleeding edge?
Not really, it’s used in prod in lots of places.
Express under the hood vs Co
As pointed out by Johnathan Ong (the current maintainer), the way in which Express works is by monkey-patching the http request and response objects as provided to you by the underlying node http core. While this is necessary, it is less than ideal because it’s an ugly approach, as mentioned by it’s primary author.
The Koa approach hands the asyncronous functionality over to generators. Flow control is maintained by the exterior application which is separated
from the request flow. It fetches from the generator’s .next()
, performs the async operation and hands it back to the generator. This inversion of the
async call allows, from the developer’s perspective, the neat and concise flow control.
This is important from a mental picture. NodeJS is nontrivially difficult to handle when doing a large number of async operations. Some requests must be handled in parallel, others ought to be serial. Promises don’t help with this and the async library and friends, while making it possible, aren’t the easiest to handle.
Koa’s reliance upon the co
flow control module allows for wonderfully elegant simplicity when dealing with this, allowing the developer to
send requests in series or parallel by simply calling yield
. Callbacks and nested functions become unnecessary as a result.
This is not to make the argument that it’s perfect or even the best async programming experience available, however insofar as Node is concerned, it is better than the alternatives for now.