DEV Community

CarolinaCobo
CarolinaCobo

Posted on

Callbacks, Promises & Async/Await

I’m currently diving more deeply into how JS works and I’ve been watching Will Sentance’s JavaScript: The Hard Parts course. One of the sections is about Promises and Async/Await and I have to say it still was a bit of a mystery for me, so after watching the section of the course I decided I’d like to do a bit more reading on it on the MDN docs.

I think to start, callbacks would make sense to see how it’s improved with promises and made our life easier.

Callbacks

Function passed inside another function as an argument, that is then called in that function to perform a task.

console.log('First');
console.log('Second');

setTimeout(()=>{
    console.log('Third');
},2000);

console.log('Last');
Enter fullscreen mode Exit fullscreen mode

This snippet would return this:

First
Second
Last
Third  
Enter fullscreen mode Exit fullscreen mode

This is because the setTimeout is set up to wait for 2 seconds so the threat of execution will continue, and then come back to it. This method was good until you needed to make multiple calls, and to do so you’d have to nest the functions to get the desired behaviour. This would cause end up in what was referred to as Callback Hell or Callback Pyramid of Doom.

Promises

The MDN docs define a Promise as an object that represents the eventual completion or failure of an asynchronous operation.

A promise is a returned object to which you attach callbacks, instead of passing callbacks into a function.

Why use them?

Using promises will guarantee you some outcomes that you wouldn’t get from callbacks such as:

  • If the callback is added with a .then() it will never be invoked before the current event loop run has been completed.
  • The callbacks will be invoked even if they were added after the asynchronous operation that the promise represents has succeeded or failed.
  • You can add multiple callbacks just by calling .then() several times. They will be invoked one after another, in the order in which you placed them in the code block.
  • Chaining, which is worth getting into more detail.

Chaining

Chaining will happen when you need to execute more than one consecutive asynchronous operation and each following operation starts when the previous one succeeds. In the promise chain the then() returns a new promise, different from the original.

Nowadays, we attach our callbacks to the returned promises instead, forming a promise chain.

Keep in mind that you can chain after a catch as well.

new Promise((resolve, reject) => {
  console.log("Start");

  resolve();
})
  .then(() => {
    throw new Error("Something went wrong");

    console.log("This first");
  })
  .catch(() => {
    console.error("This second");
  })
  .then(() => {
    console.log("Do this regardless of what happened");
  });
Enter fullscreen mode Exit fullscreen mode

Important: Always return results, otherwise callbacks won’t catch the result of a previous promise. If the previous handler started a promise but did not return it, there's no way to track it anymore, and the promise is said to be "floating". It could get worse if you have racing conditions as the result that the next then is reading could be incomplete.

While in callbacks we have to track errors multiple times, in promises if there’s an error the browser will go to the .catch() block.

try {
  const result = syncDoSomething();
  const newResult = syncDoSomethingElse(result);
  const finalResult = syncDoThirdThing(newResult);
  console.log(`Got the final result: ${finalResult}`);
} catch (error) {
  failureCallback(error);
}
Enter fullscreen mode Exit fullscreen mode

And this is pretty similar to how synchronous code works, so its similarities to asynchronous build up the async/await syntax.

async function foo() {
  try {
    const result = await doSomething();
    const newResult = await doSomethingElse(result);
    const finalResult = await doThirdThing(newResult);
    console.log(`Got the final result: ${finalResult}`);
  } catch (error) {
    failureCallback(error);
  }
}
Enter fullscreen mode Exit fullscreen mode

Async/Await

The purpose of async and await is to simplify working with asynchronous code using promises by making it look like it is synchronous code (see above).

This will make it easier to build an operation from a series of consecutive asynchronous function calls, avoiding the need to create explicit promise chains, and allowing you to write code that looks just like synchronous code.

Even though the return value of an async function behaves as if it’s wrapped in a Promise.resolve, they are not equivalent.

An async function will return a different reference, but thePromise.resolve* returns the same reference if the given value is a promise.

*Promise.resolve resolves a given value to a promise, if that value is a promise, then that promise is returned.

This difference can cause problems when you want to check the equality of a promise and the return value of an async function.

Keep in mind that just like a promise chain, await forces asynchronous operations to be completed in series. This is necessary if the result of the next operation depends on the result of the previous one but if that's not the case, using Promise.all() would be more efficient.


To summarise, promises solve a fundamental problem we faced using the callback structure. They allow us to chain and simplify catching all errors, even thrown exceptions and programming errors.

Async/await simplifies working with asynchronous code by making it look like synchronous code, remember it will return a different reference than a promise which could lead to issues.


Thanks for reading, I appreciate your time. If you need any help or have any questions please reach out!

If you have any questions feel free to drop me a message on LinkedIn or send me an email. 😊

Have a nice day!

Top comments (0)