Disqus Shortname

Responsive Advertisement

Understanding Callbacks, Promises, Async/Await, and Observables in JavaScript

JavaScript offers several ways to handle asynchronous operations, each with unique features, use cases, and trade-offs. This guide will explore callbacks, promises, async/await, and observables - explaining how they work, when to use them, and the pros and cons of each approach.


1. Callbacks

What are Callbacks?

callback is a function passed into another function as an argument and executed after some operation completes. Callbacks were the first mechanism in JavaScript for handling asynchronous tasks.
function fetchData(callback) {
  setTimeout(() => {
    callback("Data received!");
  }, 1000);
}

fetchData((data) => {
  console.log(data); // "Data received!" after 1 second
});

When to Use Callbacks

Callbacks are suitable for simple async operations where the logic doesn’t involve chaining multiple asynchronous tasks. They’re widely used in event handling and basic async flows.

Pros and Cons of Callbacks

Pros Cons
Simple to implement Can lead to callback hell
Works well for basic async tasks Hard to debug and read
Synchronous or asynchronous capable Lack of error handling standard

Drawback: Callback Hell

Callback hell occurs when callbacks are nested within other callbacks, leading to deeply indented, hard-to-read code. This happens often in complex operations requiring multiple asynchronous steps.

fetchData((data) => {
  processData(data, (processed) => {
    displayData(processed, (displayed) => {
      console.log("Data displayed!");
    });
  });
});


2. Promises

What are Promises?

A promise represents the eventual result (success or failure) of an asynchronous operation. Promises provide .then() and .catch() methods to handle results and errors. They flatten the code structure, helping mitigate callback hell.

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data received!");
    }, 1000);
  });
}

fetchData()
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.error(error);
  });

When to Use Promises

Promises are ideal for chaining asynchronous tasks, error handling, and situations where you need to execute multiple async operations sequentially.

Pros and Cons of Promises

Pros Cons
Flatten code structure Can become complex with many chains
Provides built-in error handling Harder to cancel ongoing promises
Works well with async/await Promise.all may fail on one rejection

Drawback: Complex Chains

While promises help reduce callback hell, extensive .then() chains can still be challenging to read.

fetchData()
  .then((data) => processData(data))
  .then((processed) => displayData(processed))
  .catch((error) => console.error("Error:", error));


3. Async/Await

What is Async/Await?

async/await is a syntactic sugar over promises, allowing you to write asynchronous code that reads more like synchronous code. It uses await to pause execution until a promise resolves, making async flows easier to write and understand.

async function handleData() {
  try {
    const data = await fetchData();
    console.log(data);
  } catch (error) {
    console.error("Error:", error);
  }
}

handleData();

When to Use Async/Await

async/await is best for complex asynchronous flows where readability and maintainability are priorities, such as sequential async operations and error handling.

Pros and Cons of Async/Await

Pros Cons
Readable, linear code structure await inside loops may slow code
Integrated error handling with try/catch Limited parallel execution options
Works with promises and returns a promise Not cancellable mid-way

Drawback: Lack of Cancellation

If you start a long-running async function, you can’t cancel it directly with async/await. This can be problematic when dealing with tasks that might not need to finish, like ongoing API calls or timeouts.

let task = handleData(); // You can't cancel this directly

4. Observables (RxJS)

What are Observables?

An observable is a more advanced async pattern from ReactiveX (RxJS). Observables represent a stream of values over time, allowing you to work with multiple values that may arrive asynchronously. Observables offer powerful operators for filtering, mapping, and managing data.

import { Observable } from 'rxjs';

const data$ = new Observable((observer) => {
  setTimeout(() => observer.next("Data received!"), 1000);
  setTimeout(() => observer.complete(), 1500);
});

data$.subscribe({
  next: (data) => console.log(data),
  complete: () => console.log("Completed!"),
});

When to Use Observables

Observables are best for handling streams of asynchronous data like events, user inputs, or WebSocket messages. They’re popular in frontend frameworks like Angular, which heavily leverages RxJS for state and event management.

Pros and Cons of Observables

Pros Cons
Handles multiple values over time Requires RxJS or similar libraries
Provides powerful operators Steeper learning curve
Easy to cancel subscriptions Overhead for simple async tasks

Drawback: Complexity and Learning Curve

RxJS offers a lot of functionality, but it has a steeper learning curve and more overhead than promises or async/await, especially if you only need to handle a single async result.

data$
  .pipe(
    map(data => data.toUpperCase()), // RxJS operator
    filter(data => data.includes("DATA"))
  )
  .subscribe(result => console.log(result));


Comparing the Four Approaches

Feature Callback Promise Async/Await Observable
Syntax Function-based Chained methods Linear, await for promises Chainable, stream-based
Error Handling No standard .catch() try/catch .catchError() in RxJS
Multiple Values Yes (manually handled) No No Yes
Parallel Execution Yes Promise.all() await Promise.all() Native with operators
Cancellation No Difficult No Easy
Ideal Use Case Simple async operations Sequential async tasks Complex flows, readable code Data streams, user events


Choosing the Right Approach

  • Callbacks: Great for simple, single async operations, especially in event-driven scenarios.
  • Promises: Use when you need to handle a single async result or chain multiple async operations.
  • Async/Await: Ideal for more readable, linear async code in complex flows.
  • Observables: Perfect for handling async streams, event-based data, or complex operations in frameworks like Angular.


Conclusion

Each approach has its strengths and weaknesses, so understanding them will help you select the best tool for your project. Callbacks are quick and simple but get messy with complex flows. Promises and async/await streamline code and improve readability but lack native cancellation. Observables offer powerful stream-based handling but introduce more complexity.

Choose based on the nature of your data, the level of complexity in your application, and your team’s familiarity with each tool.