Javascript Promises: Language Smells

Javascript smells worse than my armpits on an average sunny hot summer day in unventilated room. Today they keep promising that the callback hell smell would hide just right under promises. Covering a language smell, it's a language deodorant. Fortunately your close acquaintances may have taught you that stuff needs to be applied on clean skin for it to matter. This post conveys the same lesson for promises.

The Problem

Javascript has a problem with deferred and asynchronous computations. The problem is that the script itself cannot wait for results. Single stack, single thread. If you waited you couldn't do anything else. Solution is to provide a callback, but as you know this gets out of the hand and you end up with the pyramid of doom. Here's a sample:

brewCoffee(function(coffee) { 
    var cup = new Cup();
    cup.pour(coffee, function() {
        findCream(function(cream) {
            cup.pour(cream, function() {
                cup.drink(function() {
                }, function(error) {
});});});});});

I grouped the parentheses to make it little bit cleaner.. That may remind you of something. Note that pouring into the cup may fail, as well as finding cream, so it'd be actually more complex than this but I omitted those cases for clarity.

Promises Deodorant

The promises promise to help you out from this mess. Lets see how it'd look like:

brewCoffee().then(function(coffee){
    var cup = new Cup();
    return cup.pour(coffee);
}).then(findCream).then(function(cream){
    return cup.pour(cream);
}).then(cup.drink).then(function(){
}).catch(function(error){
});

The situation has improved indeed as you no longer nest callbacks or have error handling chains but it still obviously smells: You still have 4 callbacks, and 4 returns. The API changed noticeably because of the promise returned. It becomes weird because it's incompatible with the earlier concepts. There's also concerns about performance that put people reconsider it all. Your javascript codebase tends to coexist from promiseful and non-promising code.

The Problem and the Solution

Promises define an auxiliary way to describe control flows because the primary way to describe control flow in javascript cannot wait for results. Single stack, single thread. The funny thing I noticed while writing this post is that the promises deodorant is actually a bad implementation of continuations.

The actual problem javascript has is that it keeps the single stack model because it matches the machine architecture, it runs slightly faster than otherwise. Likely not fast enough to offset the performance issues coming from promises.

The promise represents an operation that hasn't completed yet, but is expected in the future to do so. If the promise is broken then it has to be catched. It represents exactly what a function call that hasn't returned yet is representing. If you could somehow suspend the execution..

In scheme programming language you can suspend execution with continuations. The idea is that you suspend the execution inside a function. Once the time is ripe, you can restart from where you left to. Scheme provides restartable continuations, so you can resume into the same state over and over again. You may remember the dreaded call/cc.

If you don't concern yourself with restartable continuations then the interface becomes little bit simpler, more like that in python greenlets. Greenlets convey the same behavior as continuations except that nothing is really captured. Instead you can get yourself a greenlet, which represents a handle into execution state and you may switch between this kind of "tasks".

Greenlets or Continuations

In python I may still write blogs or read email while the coffee is brewing. I don't have the problem of javascript and I still can do it all in a single thread with the primary control flow constructs of python:

coffee = brewCoffee()
cup = Cup()
cup.pour(coffee)
cup.pour(findCream())
cup.drink()

I can do it like this because of greenlets. So I no longer have a single stack. Instead I've got a acyclic stack tree. If you could do the same in javascript, your brewCoffee would look like this inside:

brewCoffee = function() {
    var task = getcurrent();
    asyncBrewCoffee(
        task.switch,
        task.throw);
    return eventloop.switch()
}

I can do it this way because the getcurrent() gives me the current greenlet. I may pass it into a callback and return the control back to the eventloop. Once the control returns, the brewCoffee only then returns with the coffee.

No, Promises are not superior

"But with promises I can concurrently wait for many things at once!" So do I. The map schedules a schedules new greenlet for every thing to do and suspends the current task. Once they all evaluate, or some of them fails, the current task resumes:

thingsToDo = [
    brewCoffee, burnYourClothesWithIron,
    putSocksOn, flushTheToilet
]
results = schedule.map(call, thingsToDo)

Greenlet based eventloops allow the exact same things what promises are doing, except that they actually blend in properly with everything else.

Promises might have other uses but they really will never be as clean as the code samples I gave. I hope you see the point of this post: If your primary control flow construct doesn't work, don't make an another. Instead fix the one you already had!

Similar posts