DEV Community

Jesse Warden
Jesse Warden

Posted on • Updated on • Originally published at jessewarden.com

Async/Await vs Promise.then Style

I see a lot of new, veteran, and non-JavaScript developers confused about the 2 styles of writing Promises in JavaScript. I wanted to cover what both style offers, why you’d use one or the other, and why you typically should choose one and not mix both together. Promises are a deep topic, so this isn’t a guide on the various ways Promises work, but it does include the basics.

What Is a Promise?

A Promise is a type of Object in JavaScript that holds a value. It’s typically used for values that may not be immediately available, such as JSON from an HTTP call, geolocation data, or the contents of a file read from disk. Other times you put a value into a Promise so you can start chaining it into other functions.

Why do they even exist?

The short answer is that JavaScript in the web browser needs to show you things and allow you to interact with things while other things are downloading. If the language froze on every request or user interaction, it’d be a slow and horrible to use interface.

The longer answer is how it works in other languages. In other languages, when they do some type of I/O, like loading data from the internet, reading files, they block, or pause that line of code. The mechanics may differ per language, but the effect is the same: no other code below that line runs until that operation finishes, it fails, or someone just forcefully quits the program.

Here’s Python loading some data:

result = requests.get('https://api.github.com/user')
print("done")
Enter fullscreen mode Exit fullscreen mode

Note that line 1 will pause the program. Python will go run the HTTP call. The result variable won’t be set yet. If the HTTP get call takes 10 years, then in 10 years, you’ll see the “done” print statement appear.

This effect compounds on itself. Watch what happens if you sleep a bunch of times:

print("What")
sleep(1)
print("is")
sleep(2)
print("up,")
sleep(3)
print("yo!?")
Enter fullscreen mode Exit fullscreen mode

You’ll immediately see “What”, but the “is” takes a second. The “up” takes another 2 seconds”. It takes at least 6 seconds to see “yo!?”.

This feature in blocking languages has some pro’s and con’s. The pro is, all your code is very “easy to follow”. It’s clear what is happening and when. It’s in the order it’s listed. The con is, nothing else can happening while that blocking operation is happening.

nothing else can happening while that blocking operation is happening.

… and that is why JavaScript in web browsers don’t do that. I just went to CNN.com and it loaded 170 things: HTML, CSS, JavaScript, JSON, fonts, and AJAX calls. While it took 4 seconds to load all 6 megs, I could immediately read the page and click links. It took another 6 minutes to load some additional 2 megs of higher quality images, advertising images & text changes… all while I’m still reading and possibly interacting with the page.

If it were written in Python, I’d have to wait 4 seconds for everything to download… maybe. If it had to load 1 thing at at time, it’d take a ton longer than 4 seconds. Then some additional time for everything to render, and ONLY then could I click a link. If I accidentally clicked a link while an advertisement was changing, I may have to wait some time too. Interacting with video players or image carousels would be worse.

Let’s compare our JavaScript to the above Python:

result = fetch('https://api.github.com/user')
console.log("done")
Enter fullscreen mode Exit fullscreen mode

Note that the “done” appears instantly, whether the fetch call takes 1 second or 10 years. This is because the V8 engine JavaScript uses for the Browser and Node.js passes that off to a background thread to handle. At some random point in the future, that result will have the value set internally. Like a ripe avocado 🥑, except it can’t ever spoil.

Notice because of how JavaScript works, there is no equivalent sleep command although I suppose you could hack one in the browser using a type of prompt.

How do you use them?

Now that you know why they exist, how do you get that value out of it, and when do you know it’s ready? Using then and catch… typically with callbacks. In the past, JavaScript heavily used the callback or events style to handle asynchronous things. Now that the Promise is mostly the de-facto standard for new JavaScript Browser & Node.js API’s, they just assumed people would flock to this new style of asynchronous programming… since even Promises still use callbacks.

fetch('https://api.github.com/user')
.then(
  function(result) {
    ...
  }
)
.catch(
  function(error){
    console.log("error:", error)
  }
)
Enter fullscreen mode Exit fullscreen mode

The above is a typical Promise. The fetch call is makes an HTTP GET call to some server, and at some point in the future, it’ll either give you the result or the error. Note the then or catch is called for us. We don’t have to do anything, just define the callbacks and wait. If anything goes wrong in the fetch, our catch will be called. If we screw something up in the then, that too will fire the catch. This is part of Promises having built in error handling (think of a try/catch that works for asynchronous code).

Often, people view Promises as just yet another call. The fetch either gets them data, or breaks. Promises have built in deeper meanings and uses, but that’s ok; you don NOT need to know those meanings to effectively use them. Scientists still don’t necessary grok exactly how quantum mechanics works, but we did build memory chips to lock electrons in particular states to store temporary info so… you know… computers can do this thing called “work”. Ignorance is bliss and ok.

Why chain them?

Promises enable Railway style programming in JavaScript (also called chaining or function composition). However, most don’t even know they’re doing that and that’s ok. The tl;dr; for how it works is whatever you return inside of a then will come out of the next then. You can define thisthen yourself, or let someone else do it whoever is consuming your Promise. Note in our above HTTP call, if we want to get the JSON out, we have to parse it first by calling the json parse method.

function(result) {
  return result.json()
}
Enter fullscreen mode Exit fullscreen mode

As long as you don’t return a Promise that has failed, ANYTHING will come out of the next then; a resolved Promise, a boolean, some class instance, undefined… whatever. Let’s wire that in:

fetch('https://api.github.com/user')
.then(
  function(result) {
    return result.json()
  }
)
.catch(
  function(error){
    console.log("error:", error)
  }
)
Enter fullscreen mode Exit fullscreen mode

Cool, but… how do we get at the parsed JSON? Well, again, we’re in a Promise, so we just create another then where the JSON will come out:

fetch('https://api.github.com/user')
.then(
  function(result) {
    return result.json()
  }
)
.then(
  function(jsonHere) {
    console.log("jsonHere:", jsonHere)
  }
)
.catch(
  function(error){
    console.log("error:", error)
  }
)
Enter fullscreen mode Exit fullscreen mode

The cool thing is if the json method fails, or your function that messes around with the JSON fails, the single catch handles both errors.

Why don’t people like that style anymore?

It can sure seem like there is a large movement across many blogs and social media that developers prefer the async/await style which we’ll show in a minute. Here are some of the common complaints you’ll see.

  • Promises chains are verbose.
  • async/await is cleaner.
  • async/await results in less code.
  • Promises are hard.

Each one of these have a lot in common, but I’ll cover each because I think it’s important to discuss the nuances.

Promise Chains Are Verbose

One thing JavaScript developers did very early on was the callback pattern; defining functions in functions for any asynchronous work. The most common was a click handler in jquery:

$( "#target" ).click(function() {
  alert( "Handler for .click() called." )
})
Enter fullscreen mode Exit fullscreen mode

This technique of creating anonymous functions (functions that don’t have a name, also called unnamed functions or function expressions) became very common. Additionally, in the Node.js world, you’d create smaller functions that would return some type of value to be used in a stream later on. Async function? You’re going to be using a callback.

The JavaScript language community settled on a new type of function called an Arrow function that, among other things, could help the verbosity here. Specifically, less to type and no need for the return keyword if it’s just 1 line. Let’s re-write our above using Arrow functions:

fetch('https://api.github.com/user')
.then(
  result =>
    result.json()
.then(
  jsonHere =>
    console.log("jsonHere:", jsonHere)
)
.catch(
  console.log
)
Enter fullscreen mode Exit fullscreen mode

We even abandoned any function in the catch and just passed in console.log, heh. Arrow functions do help with the verbosity aspect, especially if you remove all the whitespace I added for readability:

fetch('https://api.github.com/user')
.then( result => result.json()
.then( jsonHere => console.log("jsonHere:", jsonHere) )
.catch( console.log )
Enter fullscreen mode Exit fullscreen mode

SMUSHHHHHH

Async/Await is Cleaner

Programmers, myself included, are notorious for taking broad liberties with what a word means to them. Just like one man’s trash is another’s treasure, one woman’s clean code is another woman’s horribly written code. While there is a book called Clean Code, many openly disagree with it. My clean code I wrote back in my 20’s is gross to look at now, even with my historical context of “I was young, inexperienced, and given the tools I had at the time”.

However, the real reason many programmers say this is Promises are hard, and imperative code is easy for them to read and prevalent in our industry. Python, Ruby, Lua, non-heavy OOP Java, Go… they all HEAVILY follow the imperative or procedural style of coding. Revisiting our Python example:

print("What")
sleep(1)
print("is")
sleep(2)
print("up,")
sleep(3)
print("yo!?")
Enter fullscreen mode Exit fullscreen mode

Fast thing, then a slow thing, then a fast thing, then a slower thing, and so on. Easy to read from top to bottom, code happens in order, and you can memorize and plan for the slow things… but that doesn’t affect the order. Line 1, 2, and 3 run in the order they’re written.

This mentality is ingrained in how many developers think, just like native English speakers who read left to right. Asynchronous programming is hard, different, and requires a lot of practice to wrap your head around.

Writing our above in async/await style:

const result = await fetch('https://api.github.com/user')
const jsonHere = await result.json()
Enter fullscreen mode Exit fullscreen mode

Much smaller. Much “easier to read”, or more accurately, “less to read”. Now, the above is 90% of async/await tutorials, but if I’m TRULY re-writing the above, it actually looks like:

try {
  const result = await fetch('https://api.github.com/user')
  const jsonHere = await result.json()
} catch(error) {
  console.log("error:", error)
}
Enter fullscreen mode Exit fullscreen mode

Still, many procedural/imperative programmers understand how try/catch works. They can read from top to bottom, knowing if anything blows up, it’ll be inside the catch block. To them and their order of operations, non-asynchronous programming mentality, this looks cleaner.

Async/Await is Less Code

It certainly can be as you’ve seen above. Less code, while not definitive, does have lot of qualitative evidence in our industry that less code is considered better, regardless of language. That intrinsic value means async/await already before it’s used is perceived to be better. The only time async/await starts to get verbose is when you start using many try/catches when you’re trying to target a specific error, or you start nesting them, just like ifs, and you start using things like let to compensate for potential hoisting.

try {
  const result = await fetch('https://api.github.com/user')
  try {
    const jsonHere = await result.json()
  } catch(parseError) {
    console.log("failed to parse JSON:", parseError)
  }
} catch(error) {
    console.log("Failed to fetch the JSON:", error)
}

Enter fullscreen mode Exit fullscreen mode

… again, though, those from error prone languages like Java/C#, and in some cases Python/Ruby, that style of Exception handling may be normal for them. The await blocks fit nicely in that paradigm.

Promises Are Hard Or Aren’t Needed As Much?

Promises, and asynchronous programming is hard. Promises have all kinds of functionality many developers have no idea what it is or why the need it. With the rise of Serverless, many cloud providers make handling concurrency their problem. Suddenly, JavaScript or Elixir/Erlang or Scala/Akka’s abilities to do multiple things at once no longer matter as much. You can just use Python and spawn more Lambdas to run at the same time. If you’re heavily in this world, where is your motivation to learn Promises? If you’re into Python, where is your motivation to learn asyncio if AWS does it for you?

Why do people still use the old style?

There are 4 reasons why I continue to use the old style.

  1. I’m a Functional Programmer
  2. Promises have built-in error handling, async/await does not
  3. Promises enable railway programming
  4. Promises enable, and will eventually be enhanced, by pipeline operators. Partial applications fit nicely here.

First and foremost I’m heavily into the Functional Programming style. While JavaScript isn’t a functional language, it supports everything you need to make it work like one. Functional programming has a lot of rules, and Promises help you follow these rules; async/await sync helps you break those rules. Namely, intentionally using try/catch and condoning null pointers.

Second, functional programming doesn’t have a concept of throwing errors (F# has it to be friendly with their C# cousins). This means when you have errors, like Lua or Go, you return them. Unlike Go, you don’t end up with gigantic verbose procedural code; it’s just another link in the Promise chain. Async/await can’t pass errors; you’re expected to either throw, or just don’t have errors.

Third, Promises enable really advanced function composition, whether synchronous or asynchronous code. This style was really popularized when RxJS first hit the scene; and made it easier for developers to not care if code was sync or async; it just worked together seamlessly. Since a lot of what you do in Functional Programming is take some data in and return different data out, you start getting these large pipes that end up being 1 big wrapper function. Promises are perfect for that. If you change your mind later, you can just modify a then or add a new one without affecting the public API; your catch is still there in case something blows up, or you intentionally return a rejected Promise. This allows you to write FP code, but allow those who have no idea what you’re talking about to “just use a Promise”. “Can I use async/await?” “Sure.”

Fourth, JavaScript’s continued development is being really kind of FP developers. While it may never come to fruition, a popular operator in other FP languages is called the pipeline operator. And because it’s made for JavaScript, it works with sync or asynchronous code. If you know anything about currying and partial applications, it helps in creating re-usable functions that reduce the verbosity of code.

For example, if you’re parsing an Array, you may use the built-in Array.map function:

.then(
  items =>
    items.map(
      item =>
        item.toUpperCase()
    )
) 
Enter fullscreen mode Exit fullscreen mode

Because Promises embrace function pipelines, you can use a partial application, such as what Lodash FP offers to rewrite it:

.then(
  map(
    invoke('toUpperCase')
  )
)
Enter fullscreen mode Exit fullscreen mode

Another, simpler example, a Promise’ then or catch wants a function. So most developers will do this:

catch(
  function(error) {
    console.log(error)
  }
)
Enter fullscreen mode Exit fullscreen mode

or using Arrow functions:

catch(
  error =>
    console.log(error)
)
Enter fullscreen mode Exit fullscreen mode

… but, why? Why define a function just to call console.log? Just have the Promise call it for you:

catch(
  console.log
)
Enter fullscreen mode Exit fullscreen mode

Now, using pipeline style, we can re-write our above code to:

fetch('https://api.github.com/user')
|> result => result.json()
|> console.log
Enter fullscreen mode Exit fullscreen mode

Now, yes, you’ll need error handling, but if you’re truly writing FP style code, you won’t have errors. Using partial applications, you could change the above using Lodash/FP to:

fetch('https://api.github.com/user')
|> invoke("json")
|> console.log
Enter fullscreen mode Exit fullscreen mode

When you see |> think of a .then, just less code, heh.

Why shouldn’t I mix styles?

The short answer is because it makes the code hard to read/follow. The async/await syntax is “top to bottom, 1 line after the other” whereas the Promise code is “then or catch, and I often have no idea why I’m returning things…”. Once you start mixing it, your brain has to read half the code in top to bottom style, and other parts of the code in async “all over the place style”, and then track down where the return value, if any, is. Many async/await developers do not religiously return values because they either come from an Object Oriented Programming background which is full of Noops (functions that don’t return a value) ...or they are just doing what a lot of us front-end devs do, and create a lot of side effects, so there is no need to return a value.

Smush all that together and you’re like “what is even going on”.

await fetch('https://api.github.com/user')
.then(
  result => {
    result.json()
  }
)
Enter fullscreen mode Exit fullscreen mode

The above… did they accidentally forget to have a variable capture what fetch returns? Do they KNOW fetch returns something? They probably want the parsed JSON, but why aren’t they returning it? If they did set a variable, they still wouldn’t get it because result.json() isn’t returned.

The above paragraph is what your brain has to do. Hell with that. Just pick a style, and use it.

The good news? All Promises support async/await style. If you need to write async code; cool, you can use async/await if you want to. If a library is all written in Promises older style, you can use it using async/await. People consuming your code can use either style too.

Top comments (1)

Collapse
 
psiho profile image
Mirko Vukušić

Nice article. Thx for sharing your thoughts. Im one of those not really digging async/await. I still prefer then/catch. I'll agree async/await is cleaner only till first try/catch (not even till second nested one). So I just reached the point where I want to find a way for my IDE to stop suggesting me to convert to async/await