The Difference Between JavaScript Callbacks and Promises

Abstract image used to represent The Difference Between JS Callbacks and Promises
Image by Adam Wilson.

A callbackbased API invokes the function supplied to it. A promisebased API returns a promise, and that returned promise is a value callers can compose, await, and attach rejection handling to. Both approaches coordinate work whose result may arrive later, but they expose that work differently.


What are Callbacks?

A callback is a function supplied to other code for that code to invoke. It may run synchronously, asynchronously, or in response to an event. The examples below use TypeScript annotations to describe their inputs and outputs; compile them to JavaScript before running them.

Example of a Callback

function fetchData(url: string, callback: (data: string) => void): void {
  setTimeout(() => {
    const data = `Data from ${url}`;
    callback(data);
  }, 1000);
}

fetchData("https://api.example.com", (data) => {
  console.log(data);  // Output: Data from https://api.example.com
});

In this example:

  • The fetchData function simulates an asynchronous operation using setTimeout.
  • The callback function is executed when the data is ready, allowing us to process it.

Downsides of Callbacks

  • Callback Hell

    : Nesting multiple callbacks can result in deeply nested and hardtoread code.
  • Error Handling

    : Managing errors across multiple callbacks is cumbersome and often inconsistent.

What are Promises?

A promise is a JavaScript object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Promises offer a more structured and readable way to handle asynchronous tasks.

Example of a Promise

function fetchData(url: string): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = `Data from ${url}`;
      resolve(data);
    }, 1000);
  });
}

fetchData("https://api.example.com")
  .then((data) => {
    console.log(data);  // Output: Data from https://api.example.com
  })
  .catch((error) => {
    console.error("Error:", error);
  });

In this example:

  • The fetchData function returns a Promise.
  • The then method handles the resolved value, whilst catch handles errors, making the code more structured.

Advantages of Promises

  • Chaining

    : Promises allow chaining multiple asynchronous operations using .then().
  • Error Propagation

    : Errors can be caught at any point in the chain with .catch(), providing a cleaner errorhandling mechanism.

Key Differences Between Callbacks and Promises

FeatureCallbacksPromises
SyntaxFunction passed as an argumentObject with .then() and .catch() methods
ReadabilityCan lead to nested code (callback hell)Cleaner and easier to follow with chaining
Error HandlingMust handle errors manually at each stepCentralised error handling with .catch()
FlexibilityLimited to specific callback patternsSupports chaining and more complex flows

When to Use Callbacks or Promises

Use Callbacks When:

  • You're working with older APIs or libraries that don't support promises.
  • The task is simple and doesn't require chaining or complex error handling.

Use Promises When:

  • You're writing modern, maintainable code.
  • The task involves multiple asynchronous steps that benefit from chaining.
  • You want better error handling and improved readability.

Moving Forward: async/await

Whilst callbacks and promises are both valid approaches, modern JavaScript developers often use async/await, a syntactic sugar built on top of promises to simplify asynchronous code further. For example:

async function fetchData(url: string): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = `Data from ${url}`;
      resolve(data);
    }, 1000);
  });
}

async function getData(): Promise<void> {
  try {
    const data = await fetchData("https://api.example.com");
    console.log(data);  // Output: Data from https://api.example.com
  } catch (error) {
    console.error("Error:", error);
  }
}

getData();

This approach combines the benefits of promises with a synchronous style, which makes what might otherwise be relatively complex code all the easier to both read and maintain.


Wrapping Up

Key Takeaways

  • Callbacks are functions invoked by the receiving code, synchronously or later. An API may invoke a callback once or repeatedly.
  • Promises provide a more structured approach with chaining and centralised error handling.
  • async/await builds on promises, simplifying asynchronous code further.
  • Choose the right approach based on your project's requirements and complexity.

Callbacks remain a natural fit for repeated events and other APIs that invoke supplied behaviour. Promise handlers passed to then() and catch() are callbacks too, but each promise represents one eventual result. async/await can make a sequence of promisebased operations easier to read. Native promises are not themselves cancellable; any cancellation needs support from the underlying operation.


Want to find out more?

If you need senior handson support with a complex React or Next.js platform, migration, performance issue, or technical SEO problem, send me the context and I'll tell you where I can help.