JS: Promises, async/await, and functional programming.

Unlike a lot of the internet, I happen to enjoy writing Javascript. I’m also a fan of functional programming; from a practical point of view, and from an aesthetic point of view. In the art of code, functional is beautiful.

Unfortunately, like most JS developers, my love of functional code is at odds with the asynchronous nature of the language.

Honestly, how does one find a suitable picture that represents functional programming? This is pretty though.

Here’s the challenge:

  1. The web is inherently asynchronous. Waiting for network requests, user input or animations — they all need to happen without holding up the rest of our code. 
    (Aside: Async is not unique to the web of course. And as any programmer who has dealt with multithreaded systems with tell you: It’s never simple.)
  2. Functional programming doesn’t naturally map to asynchronous tasks. Why? A pure function should have two attributes: 
    A) It should be deterministic: For a specific input, it should always produce the same output.
    B) It should not have side effects: that is, a function should not affect anything outside itself. Further, it should not rely on anything in the global state.

Here are some examples:

// Functional - Deterministic and side-effect free.
function add(x, y){
return x + y;
}
// Not functional - Not deterministic (still side effect free)
function addRand(x){
return x + Math.random();
}
// Not functional - Deterministic, but has side-effects
var k = 6;
function addToGlobal(x){
k += x;
return k;
}
// Not functional - Not deterministic and has side-effects
var k = 6;
function addRandToGlobal(x){
k += (x*Math.random());
return k;
}

How does this conflict with asynchronous programming?

Let’s assume you want to calculate something based on an asynchronous request: the value of a user’s ETH wallet (Ethereum crypto currency tokens) in USD. You would take the total number of ETH, and multiply it by the most recent exchange rate — which you get from an API somewhere.

var ethWallet = 2.3;
function getETHinUSD(){
let rate = api.ethPrice('USD');
return ethWallet*rate;
}

There are a few issues here.

First and foremost, event though there isn’t a side-effect, ethWallet is still a global variable, which we want to avoid. Instead, let’s pass it to the function as a parameter.

function getETHinUSD(ethWallet){
let rate = api.ethPrice('USD');
return ethWallet*rate;
}

Second, assuming the api.ethPrice() function is asynchronous, this code will probably just throw an error. This is because rate will not have a value until api.ethPrice() finishes, but there is nothing telling our code to wait for api.ethPrice() , so the function will return the result of ethWallet*rate , before rate has a value.

There are two ways to fix this. Promises, or async/await.

Promises

I’m not going to go into detail on promises here, there are much better resources out there. But to remind you:

A promise is simply an object that represents the eventual completion (or failure) of an asynchronous operation, and its resulting value.

In a nutshell, Promises either complete (resolve) or fail (reject). We can do something when a promise completes by using the then() function, and do something when it fails by using the catch() function.

Let’s assume api.ethPrice() returns a Promise. In the last example, rate would become a Promise object, which would have a then() function. 
We could call rate.then( fn(result) ) with a callback function called fn which will receive result when the promise completes. 
OR
We could implicitly treat api.ethPrice() as a promise, and use then directly:

function getETHinUSD(ethWallet){
return api.ethPrice('USD').then(function(rate){
return ethWallet*rate;
})
}

Promises also do this funky thing called chaining. The then function also returns a promise, which means we could do something else asynchronously inside that inner function and have a then after that too. This chain also allows us to handle errors nicely. We can just put one catch at the end, and it will handle any asynchronous failures:

function getETHinUSD(ethWallet){
return api.ethPrice('USD')
.then(function(rate){
return ethWallet*rate;
})
.catch(function(error){
console.log("Something went wrong:", error);
return false;
})
}

Lovely, but there’s still one problem: It’s not even close to functional.

Deterministic? getETHinUSD will return a different value at different times — even with the same input. That’s OK if we expect the USD exchange rate to change over time, but that’s not all. If the api request fails, we don’t get the same value, but we don’t even get the same data type!

Side-effects? The function getETHinUSD itself does not affect our global state, but what about the inner function passed to then ? It relies on the value of ethWallet, which cannot be passed directly. 
And what about the api call itself? How do we know this function isn’t affecting the global state of our app through some other method? Maybe by requesting the ETH price, it updates a request counter for our user state, limiting the number of requests we can make, preventing api access after X requests, or just invalidating our credentials and effectively logging us out of the whole api?

Async/await

There is another approach that involves creating an asynchronous function. Again, the details are beyond the scope of this article, but are described excellently elsewhere. Again, to jog your memory:

An asynchronous function is a function which operates asynchronously using async/await syntax… 
… using async functions is much more like using standard synchronous functions.

Kurzgesagt, we can user the async keyword to identify a function that will need to wait for something in it’s body to complete. Within the async function, we use the await keyword to identify what we want to wait for.

Once again, starting with:

function getETHinUSD(ethWallet){
let rate = api.ethPrice('USD');
return ethWallet*rate;
}

We identify getETHinUSD as an asynchronous function, and we wait for the result of api.ethPrice() :

async function getETHinUSD(ethWallet){
let rate = await api.ethPrice('USD');
return ethWallet*rate;
}

This already looks really nice — right? 
What happens if the api fails? Well, we have to use a good old try/catch.

async function getETHinUSD(ethWallet){
try{
let rate = await api.ethPrice('USD');
return ethWallet*rate;
} catch (error){
console.log("Something went wrong:", error)
return false
}
}

Is this deterministic? Not if the we assume the api returns a different value over time and never fails.

Are there side effects? Possibly, but at least visually, we don’t see the same inner function accessing the parent function scope. However, because async/await still uses promises under the hood, it’s technically still happening — it just doesn’t affect us.

The api may still have side effects, but we don’t have a choice about that.

Side by side comparison

Using Promises:

function getETHinUSD(ethWallet){
return api.ethPrice('USD')
.then(function(rate){
return ethWallet*rate;
})
.catch(function(error){
console.log("Something went wrong:", error);
return false;
})
}

Using async/await:

async function getETHinUSD(ethWallet){
try{
let rate = await api.ethPrice('USD');
return ethWallet*rate;
} catch (error){
console.log("Something went wrong:", error)
return false
}
}

Some observations

  1. The Promise version is longer in terms of lines of code by a whopping single line. I’ve seen many blog posts calling this a win for async/await, but to be honest, that is not going to make much difference in a codebase of 10,000+ lines.
  2. The async/await version is still using promises, they are just hidden in the syntax.
  3. The async/await version looks more like our synchronous version, without handling the asynchronous api call. Is this a win? Many blogs seem to think so. I personally don’t. 
    At a glance, it is easy to tell that there is something odd happening with the Promise version. The difference is subtle, but straight away, we know that something asynchronous is happening — By structure rather than syntax, we know rate will not have an immediate value. 
    The argument could, of course, be made that the literal inclusion of the word async makes it obvious enough.
  4. In a longer chain of asynchronous operations, what are these two approaches going to look like? 
    In my current day-job codebase, we have a few key controller functions with many asynchronous steps. 
    Imagine creating a new widget:
    a) You need to validate the user (async)
    b) then You need to add the new widget to the database.
    c) then You need to update 3 gadgets that reference the new widget.
    d) then You need to notify 2 users that are using each gadget (6 in total) that a widget has been created.
    e) then You need to log that this has all happened successfully. 
    If any one of those steps fails, we want to log a different message and role back as much as possible.
    Using async/await, we would probably put each await call in a single try block and handle the catch at the end. But what if we wanted separate catch blocks for some awaits and not others? Imagine what that would look like! 
    Now consider each step happens in a neat then() function, and separate failures can be handled by individual catch()’s, all chained together.

What about being Functional?

The fact is that if you have a non-deterministic api call, you simply cannot write a pure function. However, in JavaScript, functional programming is always going to be a best-effort situation. The language is not designed to be truly functional.

No matter what approach you use, you can separate your function code from your non-functional code. Be functional where you can to maintain an understandable, maintainable codebase.

When you think functional javascript, you probably also think function chaining. Promises allow us to chain asynchronous sections of code in a functional way. We can treat each then like a function returned by the previous then (even if it’s actually a method of a Promise object…).

<Array>.map().filter().sort()
<Promise>.then().then().catch().finally()

See the similarity? Using await encourages you not to chain asynchronous requests, rather to use intermediate values.

let intermediateValue1 = await asynchronousFunction1() ;
let intermediateValue2 = await asynchronousFunction2() ;
let intermediateValue3 = await asynchronousFunction3() ;

It’s procedural. 
(Not a bad thing! But the topic of this post is being functional…)

This is useful in certain situations, such as using intermediateValue1 as an argument of asynchronousFunction3(). You end up storing the result in an intermediate variable outside of the promise chain anyway. This is a huge benefit of async/await — but it’s less functional in my opinion.

Of course, because await returns an implicit promise anyway, you could turn it into a promise chain:

await asynchronousFunction().then(...);

Best of both worlds, or unholy hackery? I’ll let you decide for yourself.

Conclusion

This might look like I have a preference for Promises, but to be honest, it all boils down to one thing: Use the right tool for the job!

  • If your code base already uses one approach exclusively, continue using that. It’ll save somebody a good deal of confusion.
  • If one approach is clearly easier to understand in a given context, use the more obvious solution!
  • If in doubt, ask a colleague. If you don’t have a colleague (lucky you) try both and see which feels better.

Thanks for reading!

I’m Aidan Breen, and I run a software consultancy in Dublin, Ireland.

If you enjoyed this post, consider signing upto my personal mailing list for less than monthly updates.

Many thanks to Mike van Rossum for proof reading this post and providing really valuable and insightful feedback.