DEV Community

Cover image for JavaScript Callbacks vs. Promises vs. async/await: Choosing the Right Approach
Shashwat
Shashwat

Posted on

JavaScript Callbacks vs. Promises vs. async/await: Choosing the Right Approach

Greetings everyone, I am going to write some basics about sync and async functions in javascript.

Javascript is a single threaded asynchronous programming language, which means it can only execute one line of code at a time. Single threaded nature of JS has both advantages and disadvantages. On one hand, it makes JS easier to learn and understand but on the other hand it can limit the performance of JS programs specifically when they are performing long running tasks.

To overcome the limitations of single threaded nature of javascript, developers use asynchronous techniques. Asynchronous programming allows to perform long running tasks in the background, without blocking other code executions.

There are some ways through which we can perform asynchronous programming. Techniques are follows:

  1. Callbacks (Oldest way of handling asynchronous events,
    introduced in ES5 )

  2. Promises (Introduced in ES6 IN 2015 )

  3. Async/await (Introduced in ES8 in 2017 )

CALLBACKS:

Callbacks are the oldest way of handling asynchronous events in JS. Callbacks are functions that are passed as arguments to other functions. These are executed when the function they are passed has finished executing.

_Example
_

// The main function that takes a callback as an argument
function doSomethingAsync(callback) {
  setTimeout(function () {
    console.log("Async operation completed.");
    callback(); // Calling the callback function once the async operation is done
  }, 2000);
}

// The callback function to be executed after the async operation
function onComplete() {
  console.log("Callback function executed.");
}

// Calling the main function and passing the callback function as an argument
doSomethingAsync(onComplete);
Enter fullscreen mode Exit fullscreen mode

Here are some demerits of using callback functions in 2023:

1. Callback Hell / Promise chaining:

It is a situation where a series of callbacks are nested inside each other, making it difficult to read and maintain. It makes situation worse when code needs to handle multiple asynchronous events.

2. Error Handling:

Using callback is not a good choice these days because it creates difficulty to handle errors. It makes difficult to track down the source of the error because callback may be nested deep in the code.

3. Performance:

Callbacks impact performance especially if they are nested deeply, because callback functions get executed one at a time which leads to performance bottleneck.

These are some of the good reasons Mongoose stopped accepting callbacks in their code.

PROMISES:

Promises were introduced in ECMAScript 6 in 2015.
Promises are objects that represent the eventual completion of an asynchronous task, can be chained together to create complex asynchronous workflows.

A promise has three possible states:

Pending: The promise has not yet been resolved or rejected.
Resolved: The promise has been successfully completed.
Rejected: The promise has failed.

Promises can be chained together using the then() and catch() methods. The then() method is called when the promise is resolved, and the catch() method is called when the promise is rejected.

Here is a simple example of how to use promises:

// Function that returns a Promise
function doSomethingAsync() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const randomNum = Math.random();
      if (randomNum < 0.5) {
        resolve(randomNum); // Resolve the promise with the random number if it's less than 0.5
      } else {
        reject("Error: Random number is greater than or equal to 0.5"); // Reject the promise with an error message if it's greater than or equal to 0.5
      }
    }, 2000);
  });
}

// Using the promise with `.then()` and `.catch()`
doSomethingAsync()
  .then((result) => {
    console.log("Async operation completed. Random number:", result);
  })
  .catch((error) => {
    console.error("Error occurred:", error);
  });

Enter fullscreen mode Exit fullscreen mode

In this example, the doSomethingAsync function returns a Promise. The promise resolves with a random number if it's less than 0.5 or rejects with an error message if the random number is greater than or equal to 0.5.

When using the promise, you can handle the resolved value with .then() and handle any errors with .catch(). In this case, if the promise resolves, it will print "Async operation completed. Random number: [random number]" to the console. If it rejects, it will print "Error occurred: [error message]" to the console. The setTimeout function is used to simulate an asynchronous operation with a delay of 2 seconds.

Limitations:

Promises are a powerful tool for handling asynchronous code in JavaScript. But promises still require the use of callbacks, which makes code difficult to read, maintain and handle errors as we discussed above.

To adress these limitations, async/await was introduced in ECMAScript 2017. Async/await is a syntax sugar for promises. Async/await makes it easier to write asynchronous code that is both readable and concise.

ASYNC/AWAIT

Async/await is a feature introduced in ECMAScript 2017 (ES8) to simplify asynchronous programming in JavaScript. It provides a more concise and readable way to work with asynchronous code, especially when dealing with promises.
async: It is used before a function declaration to mark the function as asynchronous. An async function always returns a Promise, implicitly wrapping its return value in a resolved Promise if it's not already a Promise.

await: It can only be used inside an async function. The await keyword is used to pause the execution of the async function until the Promise is resolved or rejected. This helps in sequential and more straightforward handling of asynchronous operations.

example

async function getResults() {
  const result1 = await fetch("https://example.com/");
  const result2 = await fetch("https://example.com/2");

  return { result1, result2 };
}

const results = await getResults();
Enter fullscreen mode Exit fullscreen mode

In this example, the async keyword is used to declare the getResults() function as an asynchronous function. The await keyword is used to wait for the completion of an asynchronous operation.

The getResults() function first makes two asynchronous HTTP requests. The await keyword is used to wait for the completion of each HTTP request. Once the HTTP requests are complete, the results are returned from the function.

The results variable is assigned the result of the getResults() function. The results variable is a promise that will be resolved with the results of the two HTTP requests.

Benefits:

  1. Improved readability

  2. Sequential execution

  3. Error handling as we discussed before

  4. Debugging

  5. Compatibility with promises

Overall, async/await was introduced to improve the developer experience when dealing with asynchronous operations, leading to more readable, maintainable, and concise code. It has become a popular choice for handling asynchronous tasks in modern JavaScript development.

Top comments (0)