DEV Community

loading...
Cover image for JavaScript - Demystifying Callbacks, Promises and Async Functions

JavaScript - Demystifying Callbacks, Promises and Async Functions

Pablo Veiga
Software Engineer since 2013 | JavaScript expert | Agile developer | Amateur writer | Beer and football lover
・4 min read

Imagine these two scenarios:

1) It is a rainy Monday and I'm alone at home, working as hell. It is around noon and my stomach starts to ache: "It's time do eat, idiot!". I grab my mobile phone and open the most famous food delivery app and ask for a pizza (be healthier, it's not even weekend, you b*tch!). I select the ingredients, choose to pay directly to the deliveryman and click "Ask for bloody pizza". I sit on the couch, do nothing, and wait for my pizza to be delivered.

2) It is a rainy Monday and bla bla bla the same as above but, while the pizza isn't delivered I decide to clean my desk, do the dishes from last night (lazy bastard!), watch some TV and practice acoustic guitar.

It is quite easy to identify the difference between the two scenarios.
In the first one, my life stops completely while I wait for the pizza and, in the second one, I do plenty of other things while it is not delivered.

These two examples demonstrate (or at least try to) what synchronism is. This is what I'm going to talk about in this article: Synchronous and Asynchronous JavaScript operations implemented using: callbacks, promises and async functions. Ta dãã!


Callbacks

Callback is a function that is passed as argument to another function (a high-order function) and it is executed when something specific occurs. A very common usage is when fetching data using Ajax, for example.
If you have ever used setTimeout or setInterval you have already used callbacks.

In the following example the callback function is passed as an argument to setTimeout and it is executed after 5 seconds.

function callback() {
  console.log("Time's up!")
}

setTimeout(callback, 5000)
Enter fullscreen mode Exit fullscreen mode

Callbacks are useful to deal with asynchronous cycles. Check the following example. Each time a function receives another function as an argument, it executes its own action and then executes the callback, which does the same thing and so on.

function orderPizza(function() {
  console.log('Pizza ordered!')

  waitForPizzaToBeDelivered(function() {
    console.log('Pizza delivered!')

      payForPizza(function() {
        console.log('Pizza paid!')

        eatPizza(function() {
           console.log('Pizza finished! :(')
        })
      })
   })
})
Enter fullscreen mode Exit fullscreen mode

The problem is what we call callback hell; because, the more complex the scenario, the more scope levels and callbacks will be necessary, making it harder to read and even maintain it, creating this "pyramid" effect.

Promises

Since the very beginning, Promises have been trying to represent asynchronous routines sequentially and implement a better error treatment. Let´s see how the example above is written using Promises.

orderPizza()
.then(function(pizza) {
  console.log(`A ${pizza.flavour} has been ordered!`)  
  return waitForPizzaToBeDelivered()
})
.then(function() {
  console.log('Pizza delivered!')
  return payForPizza()
})
.then(function() {
  console.log('Pizza paid!')
  return eatPizza()
})
.then(function() {
  console.log('Pizza finished :(')
})
Enter fullscreen mode Exit fullscreen mode

The main difference between using callbacks and promises is that, using promises we can avoid the "pyramid" effect created by callbacks within callbacks, making the code easier to understand.

At the end of each .then() it is possible to return:

  • a value like an object, an array, a string, etc.
    In this case the next then in sequence will be immediately executed.

  • another promise
    This is what we've done in the example above.
    For each then in the sequence to wait for an operation to be completed, a promise must be returned. Once the promise has been resolved, the execution proceeds.

So, to make it simpler. What exactly is a promise?

A Promise is used to represent a value and make sure that you will receive that value.
When the function finishes the required processing to return what it has promised to you, you will be warned.
Sometimes, things can go wrong (the connection is down, for example) and the value will never be delivered.

This is how a Promise is created.
We will use the simple setTimeout example.

function sendDelayedMessage(message, milliseconds) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if(message) {
        resolve(message)
      } else {
        reject('Message is empty!')
      }      
    }, milliseconds)
  })
}
Enter fullscreen mode Exit fullscreen mode

In the example above we can see that the function sendDelayedMessage creates and returns a new Promise. A Promise constructor gets a function named executor as argument which, then, gets two other functions as arguments: resolve and reject. They are "magically" injected there and you just need to call them at the right time according to your needs.

  • resolve: it must be called to inform that the promise has been deferred or "resolved". If the routine has any result, it must be passed as argument to it.

  • reject: it must be called in case there were any errors. By executing it you are informing that the promise has failed or has been "rejected".

It is possible to treat errors in then() chains using .catch():

sendMessage('Hi, beautiful people!', 5000)
.then(message => {
  console.log('Message successfully sent', message)
})
.catch(error => {
  console.error('Something wrong happened!', error)
})
Enter fullscreen mode Exit fullscreen mode

Async Functions

If Promises were created to increase readability and maintainability, JavaScript has given a huge step forward in both aspects with async functions.

Async Functions make asynchronous code look like synchronous.

Here is our bloody pizza example written using Async Functions:

async function pizzaFlow() {
  const pizza = await orderPizza();
  console.log(`A ${pizza.flavor} pizza has been ordered!`);

  await waitForPizzaToBeDelivered();
  console.log('Pizza delivered!');

  await payForPizza();
  console.log('Pizza paid!');

  await eatPizza();
  console.log('Pizza finished :(');
}
Enter fullscreen mode Exit fullscreen mode

It is only possible to use "await" within functions marked as "async"

When JavaScript finds an await instruction, it will wait for that operation to complete in order to continue with the execution flow.
Async function may be briefly explained as "syntactic sugar" for Promises.


Conclusion

There are several ways to deal with asynchronous operations in JavaScript and none of them is considered "wrong"! They all have their pros and cons. The most important is to understand how they work and when to use them properly according to the situation.

This post was heavily based on the article Asynchronous JavaScript: callbacks, promises and async functions* by Alcides Queiroz

*Written in Portuguese

I hope you liked it.
Please, comment and share!

Cover image by @ryanmfranco

Discussion (2)

Collapse
landb profile image
Branko Stancevic • Edited

Great article! But I have stupid observasion. Doesn’t await blocks the execution of the following code until it gets results?

For e.g.
await bla();
Console.log(“log after await”)

Console log will appear only when bla function is finished with execution, am I right?

If I am right, I don’t think that async is syntetic sugar to promises becase there’s a big difference: blocking the execution of the code with async and non blocking with promises :D

Collapse
vcpablo profile image
Pablo Veiga Author

Hey @landb , you take a closer look to this code example, you'll notice that the console.log will also be executed each time a promise is resolved

orderPizza()
.then(function(pizza) {
  console.log(`A ${pizza.flavour} has been ordered!`)  
  return waitForPizzaToBeDelivered()
})
.then(function() {
  console.log('Pizza delivered!')
  return payForPizza()
})
.then(function() {
  console.log('Pizza paid!')
  return eatPizza()
})
.then(function() {
  console.log('Pizza finished :(')
})
Enter fullscreen mode Exit fullscreen mode

By using async/await in the last example, we will have the same result, but in a more readable way.
This is enough to call it synthetic sugar to promises, at least in this scope.
Perhaps, in other kinds of examples, this may not apply very well.

But I see your point!
Thank you so much for participating.

Cheers!