Podcast Title

Author Name

0:00
0:00
Album Art

A Clear Guide to JavaScript Promises and Async/Await

By 10xdev team August 03, 2025

In this article, I'll show you what JavaScript promises are, why we need them, how to use the special then and catch methods, and then how to convert the same code to using the much neater async and await keywords.

The Challenge with Asynchronous Code

When dealing with simple types in JavaScript, such as strings and numbers, our code executes sequentially. We can assign a string, a number, and then combine the two values together straight away. Everything is nice and simple.

However, when writing real-world code, we often make calls to databases, open files, and speak to remote APIs over the internet. Now, longer-running tasks like this will usually not return the results straight away; they will rather return a promise.

What Exactly is a Promise?

A promise is a special type of object in JavaScript that represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

If this sounds a little bit hard to understand, maybe we can imagine it as a real-world scenario. Imagine you are at a restaurant having dinner and you ask the waiter to bring you another cup of coffee. The waiter promises to come back with your coffee. However, you can't drink it at that point; you have to wait until he returns with your coffee and the promise is fulfilled. This is the same sort of concept in JavaScript.

If, for example, you request some information from a remote API, then you will be immediately given a promise that the task will eventually either complete or fail. It's not until sometime later that the promise itself is actually resolved or rejected, and you can use the result of that promise.

A Practical Example in JavaScript

Now, imagine you were building an app that suggested things to do when you were bored. You'll be using the Bored API, which just returns random suggestions of things that you can do, along with the number of participants required. Let's just keep it really simple by going to the API, getting a suggested activity, and then logging out the activity to the console.

In this example, we're using the axios request library. The get method returns immediately, but that doesn't mean that the request has finished processing. What we have is a promise that the request will be fulfilled in the future.

This code will fail because the response object is not what we're expecting. The data and activity properties do not exist on the promise itself.

// This code will fail
import axios from 'axios';

const response = axios.get('https://www.boredapi.com/api/activity');
// Fails because 'response' is a Promise, not the final data.
console.log(response.data.activity); 

So is there any way for us to get access to the result of the request and run code when it returns? Thankfully, yes. JavaScript gives us a couple of ways to wait until a task is finished and use the result or catch any errors that occur.

Handling Promises with .then() and .catch()

The first way is by using a couple of special methods on the promise object.

  • .then(): This method is called when the task completes successfully. As a parameter, it receives the result of the task.
  • .catch(): This method is called if anything goes wrong while processing our request. This receives the error that occurred as a parameter.

So, let's replace the previous code. We'll use the axios request library and call the get method. Because get returns a promise object, we can immediately chain on the .then() method.

import axios from 'axios';

axios.get('https://www.boredapi.com/api/activity')
  .then(response => {
    // This code runs only after the request is successful
    console.log(response.data.activity);
  })
  .catch(error => {
    // This code runs if the request fails
    console.log(`Error: ${error.message}`);
  });

// This line will execute immediately, before the API responds.
console.log("Request sent!"); 

When we run this, we can see that our console log inside .then() executes in the right place.

Now, to simulate an error occurring in our request, let's replace the URL with a call to the HTTP Status API. This is really useful for testing different status codes, and we'll just request a 404 Not Found error.

import axios from 'axios';

axios.get('https://httpstat.us/404')
  .then(response => {
    console.log(response.data.activity);
  })
  .catch(error => {
    // The error is neatly caught here
    console.log(`Error: ${error.message}`);
  });

Running this, we see that our error is neatly caught by our catch method and printed out to the console.

It's worth noting that any code placed after this promise chain will be executed immediately. If we put a console log at the bottom, we'd expect it to be written out after our request returns, but actually, it gets printed out first. This is because only the code inside the then and catch methods is executed after the request returns.

A Cleaner Approach: async and await

This works fine. However, as you can see, the code isn't particularly nice to look at, and if you had a lot of complicated code inside your methods, things would soon start to get quite unwieldy.

What we need, really, is a way of receiving the results of our promises sequentially, just as if we were dealing with simple types like strings and numbers. This is where the await keyword comes in. await does exactly what it says: it allows us to wait until the promise has completed before moving on to the next line. This makes our code a lot neater and easier to read.

JavaScript requires our await keywords be used inside functions marked with the async keyword. So, let's replace our promise chain with a function marked as async.

import axios from 'axios';

async function getActivity() {
  // The 'await' keyword pauses execution until the promise settles
  const response = await axios.get('https://www.boredapi.com/api/activity');

  // This line will not run until the await above is complete
  console.log(response.data.activity);
}

getActivity();

When we call our getActivity function, we'll see that our code executes perfectly.

Simplified Error Handling

Because the await keyword allows us to move this kind of asynchronous code back into the main flow of our app, we don't have access to the specialized catch method to handle any errors that occur. So what happens if something goes wrong?

Because our code executes sequentially, we can just wrap this with a normal try...catch block.

import axios from 'axios';

async function getActivity() {
  try {
    // We'll request a 500 server error to test the catch block
    const response = await axios.get('https://httpstat.us/500');
    console.log(response.data.activity);
  } catch (error) {
    // The request error has been nicely caught and logged
    console.log(`Caught an error: ${error.message}`);
  }
}

getActivity();

Notice our request error has been nicely caught and logged out to the console.

I hope that has helped to demystify using promises and the async and await keywords in JavaScript.

Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Recommended For You

Up Next