Understanding the difference between Promises and async/await in JavaScript

Understanding the difference between Promises and async/await in JavaScript

Understanding Promises 🤝

Before the widespread adoption of async/await, Promises served as the primary solution for managing asynchronous tasks in JavaScript. A Promise in JavaScript acts as a representation of the eventual completion or failure of an asynchronous operation. It provides a mechanism for handling asynchronous operations by allowing you to attach callbacks to it, thereby mitigating the issue of nested callbacks, commonly known as "callback hell," especially in complex scenarios.

eg -

function fetchData(url) {
  return new Promise((resolve, reject) => {
    // Simulate an API call
    setTimeout(() => {
      const data = "Fake data from " + url;
      resolve(data);
    }, 1000);
  });
}

fetchData("https://api.example.com/data")
  .then((data) => console.log(data))
  .catch((error) => console.error(error));

CONS:

  1. Callback Hell: Although Promises help mitigate callback hell to some extent, complex chains of .then() calls can still become difficult to read and maintain, especially in deeply nested scenarios.

  2. Verbosity: Working with Promises often involves writing verbose code due to the chaining(also called thening) of .then() and .catch() methods, which can make the codebase harder to understand, especially for beginners.

Entering async/await 🕚

Async/await, introduced in ES2017, is a syntactic improvement built upon Promises. It enables you to craft asynchronous code that resembles and functions similarly to synchronous code, representing a substantial enhancement in terms of code readability and simplicity.

async function fetchData(url) {
  try {
    // Simulate an API call
    const response = await new Promise((resolve, reject) => {
      setTimeout(() => {
        const data = "Fake data from " + url;
        resolve(data);
      }, 1000);
    });
    console.log(response);
  } catch (error) {
    console.error(error);
  }
}

fetchData("https://api.example.com/data");

Pros:

  1. Simplicity and Readability: The async/await syntax is clear, direct, and instantly comprehensible. 📖

  2. Error Handling: It facilitates intuitive error handling through the use of try/catch blocks, simplifying the process compared to chaining .catch() methods.

  3. Debugging: Debugging asynchronous code becomes simpler with async/await, as execution pauses at await expressions, mimicking synchronous behavior during debugging sessions.

Cons:

  1. Error Propagation: In async/await, errors need to be meticulously handled, or they might be overlooked, potentially resulting in elusive bugs. 🚫

  2. Possible Blocking: Misuse of await can unintentionally block code execution, particularly when used in loops or in serializing unnecessary asynchronous operations, leading to delays in program flow. ⏳

Understanding the Difference in Execution 🖥️

Promises Execution

Promises are executed immediately upon creation. This means that when you create a new Promise, the executor function passed to the constructor is run straight away. The then-catch-finally pattern is used to handle the results of the Promise once it has been resolved or rejected.

Consider this flow:

  1. Initialization: The Promise is created and the executor function runs immediately.

  2. Pending State: Until the asynchronous operation completes, the Promise remains in a pending state.

  3. Settlement: The Promise is either fulfilled with a value (resolved) or an error (rejected).

  4. Chaining: .then() is called if the Promise is resolved, .catch() if it's rejected, and .finally() runs in both cases after the resolution or rejection.

Promises enforce a clear separation between the initiating of an asynchronous operation and the handling of its result, which can be both a strength and a complexity depending on the situation.

async/await Execution

The async/await pattern simplifies the chaining of Promises by making asynchronous code look and behave more like synchronous code. This is especially useful when you need to perform a series of asynchronous operations in a specific order.

Here’s the typical execution flow:

  1. Async Function: An async function is declared, which implicitly returns a Promise.

  2. Awaiting: Within the async function, await is used to pause the execution until the Promise resolves.

  3. Sequential Execution: Each await call waits for the previous operation to complete before executing the next line, making it easier to follow the code flow.

  4. Error Handling: try/catch blocks within an async function can catch both synchronous and asynchronous errors, giving a synchronous feel to error handling.

While async/await can make code easier to read by reducing the nesting of .then() and .catch() methods, it's important to note that it can also lead to performance issues if not used correctly, as it might introduce unnecessary waiting.
Comparing Promises and async/await

FeaturePromisesasync/await
SyntaxThenable chainSyntactic sugar over Promises, uses async and await keywords
Error HandlingUses .catch() for errorsUses try/catch blocks
ReadabilityGood for simple chains, but can get complicated with multiple chainsMore readable and looks synchronous
DebuggingCan be challenging in complex promise chainsEasier, as code can be stepped through like synchronous code
Return ValueAlways returns a Promiseasync function returns a Promise
Execution FlowNon-blocking, always asynchronousPauses at each await for the Promise to resolve, making it easier to follow the flow
Ideal Use CaseSuitable for single or less complex asynchronous operationsBetter for handling multiple asynchronous operations in a sequence
Complex Asynchronous PatternsPossible but may lead to "callback hell" if not managed properlySimplifies handling complex asynchronous code patterns
Community PreferenceWidely used before ES2017, still prevalent for simple casesIncreasingly preferred for its simplicity and cleaner code structure

Promises kick off the moment you create them, and you handle the results with .then() or .catch().

async/await makes you wait at each await for the promise to finish, which can make your code easier to follow but might slow things down if you’re not careful.

So, What Should You Use? 🤔

  • Use Promises when you have lots of async things happening that don’t depend on each other. They’re great for managing multiple things at once without creating a mess.

  • async/await is awesome when you need things to happen in a specific order, one after the other. It keeps your code clean and easy to read.