avatarSandeep Dinesh

Summary

The website content provides an overview of asynchronous code handling in JavaScript through the use of callbacks, promises, and async/await syntax, emphasizing the evolution towards cleaner and more readable code.

Abstract

The article discusses the challenges of executing asynchronous operations in JavaScript, initially demonstrating how using setTimeout without proper control leads to unpredictable execution order. It then proceeds to illustrate how callbacks can be used to manage the order of asynchronous function calls, despite the potential for creating deeply nested and difficult-to-read "Callback Hell." The article introduces Promises as a solution to the nesting problem, showcasing how they allow for cleaner promise chains and the passing of data through asynchronous operations. Finally, it presents async/await as a more intuitive and synchronous-like approach to handle promises, significantly improving code readability and maintainability.

Opinions

  • The author suggests that arrow functions enhance code readability by providing a more concise syntax for anonymous functions.
  • The article implies that using callbacks alone can lead to complex and unmanageable code, known as "Callback Hell."
  • Promises are presented as a significant improvement over callbacks, offering a clearer structure and better handling of asynchronous results.
  • The author expresses a clear preference for async/await syntax over callbacks and promises due to its resemblance to synchronous code, making it easier to understand and work with.
  • The article conveys that each successive method of handling asynchronous code (callbacks, promises, async/await) is an attempt to improve upon the limitations of the previous one.

Callbacks, Promises and Async/Await

This is my attempt to explain how to use asynchronous code in Javascript at a very high level.

Note: I’m going to be using arrow functions in this blog post. Basically I’ll be replacing anonymous functions like so:

function() {}     function(a) {}      function(a,b) {}

can be replaced with

() => {}          a => {}            (a,b) => {}

Read more about arrow functions here

Let’s say you have a function that will print a string after a random amount of time:

function printString(string){
  setTimeout(
    () => {
      console.log(string)
    }, 
    Math.floor(Math.random() * 100) + 1
  )
}

Let’s try to print the letters A, B, C in that order:

function printAll(){
  printString("A")
  printString("B")
  printString("C")
}
printAll()

You will notice that A, B, and C print in a different and random order each time you call printAll!

This is because these functions are asynchronous. Each function gets executed in order, but each one is independent with it’s own setTimeout. They won’t wait for the last function to finish before they start.

This is super annoying, so let’s fix it with a callback.

Callbacks

A callback is a function that is passed to another function. When the first function is done, it will run the second function.

function printString(string, callback){
  setTimeout(
    () => {
      console.log(string)
      callback()
    }, 
    Math.floor(Math.random() * 100) + 1
  )
}

You can see that is is super easy to modify the original function to work with callbacks.

Again, let’s try to print the letters A, B, C in that order:

function printAll(){
  printString("A", () => {
    printString("B", () => {
      printString("C", () => {})
    })
  })
}
printAll()

Well, the code is a lot uglier now, but at least it works! Each time you call printAll, you get the same result.

The problem with callbacks is it creates something called “Callback Hell.” Basically, you start nesting functions within functions within functions, and it starts to get really hard to read the code.

Promises

Promises try to fix this nesting problem. Let’s change our function to use Promises

function printString(string){
  return new Promise((resolve, reject) => {
    setTimeout(
      () => {
       console.log(string)
       resolve()
      }, 
     Math.floor(Math.random() * 100) + 1
    )
  })
}

You can see that it still looks pretty similar. You wrap the whole function in a Promise, and instead of calling the callback, you call resolve (or reject if there is an error). The function returns this Promise object.

Again, let’s try to print the letters A, B, C in that order:

function printAll(){
  printString("A")
  .then(() => {
    return printString("B")
  })
  .then(() => {
    return printString("C")
  })
}
printAll()

This is called a Promise Chain. You can see that the code returns the result of the function (which will be a Promise), and this gets sent to the next function in the chain.

The code is no longer nested but it still looks messy!

By using features of arrow functions, we can remove the “wrapper” function. The code becomes cleaner, but still has a lot of unnecessary parenthesis:

function printAll(){
  printString("A")
  .then(() => printString("B"))
  .then(() => printString("C"))
}
printAll()

Await

Await is basically syntactic sugar for Promises. It makes your asynchronous code look more like synchronous/procedural code, which is easier for humans to understand.

The printString function doesn’t change at all from the promise version.

Again, let’s try to print the letters A, B, C in that order:

async function printAll(){
  await printString("A")
  await printString("B")
  await printString("C")
}
printAll()

Yeah…. MUCH better!

You might notice that we use the “async” keyword for the wrapper function printAll. This let’s JavaScript know that we are using async/await syntax, and is necessary if you want to use Await. This means you can’t use Await at the global level; it always needs a wrapper function. Most JavaScript code runs inside a function, so this isn’t a big deal.

BUT WAIT THERE'S MORE

The printString function doesn’t return anything and is independent, all we cared about was the order. But what if you wanted to take the output of the first function, do something with it in the second function, and then pass it to the third function?

Instead of printing the string each time, let’s make a function that will concatenate the string and pass it on.

Callbacks

Here it is in callback style:

function addString(previous, current, callback){
  setTimeout(
    () => {
      callback((previous + ' ' + current))
    }, 
    Math.floor(Math.random() * 100) + 1
  )
}

And in order to call it:

function addAll(){
  addString('', 'A', result => {
    addString(result, 'B', result => {
      addString(result, 'C', result => {
       console.log(result) // Prints out " A B C"
      })
    })
  })
}
addAll()

Not so nice.

Promises

Here it is in Promise style:

function addString(previous, current){
  return new Promise((resolve, reject) => {
    setTimeout(
      () => {
        resolve(previous + ' ' + current)
      }, 
      Math.floor(Math.random() * 100) + 1
    )
  })
}

And in order to call it:

function addAll(){  
  addString('', 'A')
  .then(result => {
    return addString(result, 'B')
  })
  .then(result => {
    return addString(result, 'C')
  })
  .then(result => {
    console.log(result) // Prints out " A B C"
  })
}
addAll()

Using arrow functions means we can make the code a little nicer:

function addAll(){  
  addString('', 'A')
  .then(result => addString(result, 'B'))
  .then(result => addString(result, 'C'))
  .then(result => {
    console.log(result) // Prints out " A B C"
  })
}
addAll()

This is definitely more readable, especially if you add more to the chain, but still a mess of parenthesis.

Await

The function stays the same as the Promise version.

And in order to call it:

async function addAll(){
  let toPrint = await addString('', 'A')
  toPrint = await addString(toPrint, 'B')
  toPrint = await addString(toPrint, 'C')
  console.log(toPrint) // Prints out " A B C"
}
addAll()

Yeah. SO MUCH BETTER.

JavaScript
Asynchronous
Promises
Nodejs
Development
Recommended from ReadMedium