Back to Blog
Lesson 36 of the JavaScript: From Zero to Interactive Web Pages course
August 23, 20264 min read

Introduction to Promises: Mastering Async JavaScript Flow

Learn how to use JavaScript promises to manage asynchronous tasks effectively. Master .then() and .catch() to handle network requests and errors with confidence.

Detailed view of programming code in a dark theme on a computer screen.

Previously in this course, we explored asynchronous JavaScript basics where we used setTimeout to delay code execution. While timers are useful for simple delays, they don't help us manage the outcome of a task—like fetching data from a server.

In modern web development, "doing things that take time" is the norm. Whether you are reading a file, querying a database, or calling a weather API, your code needs a way to track the status of that operation. This is where the Promise object comes in.

What is a Promise?

A Promise is an object representing the eventual completion (or failure) of an asynchronous operation. Think of it like ordering a coffee at a busy cafe:

  1. Pending: You place your order and receive a buzzer. The coffee isn't ready, but you have a "promise" that it will be.
  2. Fulfilled (Resolved): The buzzer goes off, and you get your drink. The operation succeeded.
  3. Rejected: The barista tells you they ran out of milk. The operation failed.

In JavaScript, a Promise allows us to attach code that runs only after the result is known, keeping our main program logic clean and readable.

Using .then() for Success

Yellow letter tiles spell the word success on a vibrant blue backdrop, symbolizing achievement.

When a Promise resolves successfully, we use the .then() method to define what happens next. You can think of .then() as a callback that only triggers once the "buzzer" of your asynchronous task goes off.

Here is a basic example of creating and consuming a Promise:

JAVASCRIPT
const orderCoffee = new Promise((resolve, reject) => {
  const isCoffeeReady = true;

  if (isCoffeeReady) {
    resolve("Here is your latte!");
  } else {
    reject("Sorry, we are out of coffee.");
  }
});

// Handling the success case
orderCoffee.then((message) => {
  console.log(message); // "Here is your latte!"
});

The resolve function inside the constructor tells the Promise that it finished successfully, passing the result (the message) to our .then() block.

Handling Errors with .catch()

Real-world network requests often fail—perhaps the user is offline or the server is down. If you don't handle these failures, your application might break. We use the .catch() method to "catch" any errors that happen during the process.

Let’s update our example to simulate a failure:

JAVASCRIPT
const orderCoffee = new Promise((resolve, reject) => {
  const isCoffeeReady = false; // Simulating a problem

  if (isCoffeeReady) {
    resolve("Here is your latte!");
  } else {
    reject("Error: We ran out of milk.");
  }
});

orderCoffee
  .then((message) => {
    console.log(message);
  })
  .catch((error) => {
    console.error(error); // "Error: We ran out of milk."
  });

By chaining .catch(), you ensure that your code doesn't just crash when something goes wrong. This pattern is essential for handling asynchronous errors and maintaining a smooth user experience.

Hands-on Exercise: The Mock Request

In our project, we’ll eventually fetch weather data. Let’s practice by creating a function that returns a Promise, simulating a network delay.

  1. Create a function called fetchWeatherData that returns a new Promise.
  2. Inside, use setTimeout to wait 2 seconds.
  3. After 2 seconds, randomly decide to either resolve with an object { temp: 72 } or reject with an error message.
  4. Call your function and use .then() to log the temperature and .catch() to log the error.

Common Pitfalls

  • Forgetting to return a result: If you don't call resolve() or reject(), your Promise will stay in the "pending" state forever.
  • Assuming linear execution: Remember that everything inside .then() happens after the main code block has finished executing. Don't try to access variables defined inside a .then() from outside of it.
  • Swallowing errors: Never leave a Promise chain without a .catch(). If you do, errors will often fail silently or trigger "Unhandled Promise Rejection" warnings in the console, which can make debugging asynchronous stability much harder.

If you find yourself managing multiple concurrent requests, you might eventually need more advanced patterns like Promise.allSettled to ensure one failure doesn't halt your entire dashboard.

FAQ

Q: Can I use multiple .then() blocks? A: Yes! This is called "chaining." You can return a value from one .then() and it will be passed to the next one in the chain.

Q: Does every Promise need a .catch()? A: Technically no, but you should always include one in production code to prevent silent failures.

Q: What is the difference between resolve and reject? A: resolve signals success and moves the Promise to the "fulfilled" state; reject signals an error and moves it to the "rejected" state.

Recap

Promises allow us to write cleaner, more manageable asynchronous code. By using new Promise(), we define the task, and by using .then() and .catch(), we define exactly how to react to success and failure. You now have the tools to handle tasks that take time without blocking the rest of your page.

Up next: We will apply these concepts to the real world by learning how to use the Fetch API to pull data from actual servers.

Similar Posts