Blog Details

Home Blog Details

JavaScript Promises and Async/Await Explained with Examples

WEB DEVELOPMENT 31 Oct 2026 By Well Spring Talent Team

Introduction

Imagine building an online shopping website. When a customer opens a product page, the browser needs to retrieve product information from a server. While that request is being processed, the user should still be able to interact with the page instead of waiting for the entire application to stop.

This is where asynchronous programming becomes important in JavaScript.

JavaScript uses asynchronous operations to handle activities such as API requests, file processing, timers, and database communication without requiring the application to wait synchronously for every operation to finish.

Two important concepts make this possible: Promises and async/await. They help developers manage operations that produce results later, handle failures, and organise code that depends on multiple asynchronous tasks.

Understanding JavaScript Promises and async await is essential for frontend developers, backend developers working with Node.js, and anyone building modern web applications.

This guide explains how promises work, how to use async functions, how to handle errors, and when to execute asynchronous tasks sequentially or concurrently, with practical JavaScript examples.

Quick Answer: What Are JavaScript Promises and Async/Await?

A JavaScript Promise is an object that represents the eventual success or failure of an asynchronous operation. It can be handled using methods such as .then(), .catch(), and .finally().

async and await provide a cleaner way to work with promises. An async function always returns a promise, while await pauses the execution of the surrounding async function until the awaited promise settles. This makes asynchronous code easier to read and maintain.

1. What Is JavaScript Asynchronous Programming?

Asynchronous programming allows JavaScript to initiate an operation and continue handling other work while waiting for that operation to complete.

Consider a webpage that needs to retrieve a list of available courses from a server. The request may take some time because of network conditions or server processing.

If the application handled every operation synchronously, it could become unresponsive while waiting for certain tasks to finish. Asynchronous APIs help avoid this problem by allowing the JavaScript execution environment to manage operations and process their results later.

Synchronous vs asynchronous execution

Synchronous execution follows a sequential approach. Each statement runs after the previous statement has completed.

console.log("Step 1: Open application");

console.log("Step 2: Load dashboard");

console.log("Step 3: Display courses");

Output:

Step 1: Open application

Step 2: Load dashboard

Step 3: Display courses

Now consider an asynchronous timer:

console.log("Step 1: Open application");

setTimeout(() => {

    console.log("Step 2: Load course information");

}, 2000);

console.log("Step 3: Display page layout");

Output:

Step 1: Open application

Step 3: Display page layout

Step 2: Load course information

The timer callback runs later, allowing the remaining synchronous statements to execute first.

JavaScript uses the event loop and task scheduling mechanisms to coordinate asynchronous callbacks and promise reactions. This is why asynchronous code does not necessarily execute in the same order in which its operations were started.

2. What Is a JavaScript Promise?

A JavaScript Promise is an object that represents the eventual result of an asynchronous operation.

Instead of receiving the final result immediately, a function can return a promise that will eventually be fulfilled with a value or rejected with an error.

For example, a course website may request course details from an API. The request cannot guarantee an immediate response, so the application receives a promise representing the pending operation.

A promise can be handled using three main methods:

  • .then() handles successful completion and receives the fulfilled value.
  • .catch() handles rejection or errors in the promise chain.
  • .finally() runs after the promise settles, regardless of whether it was fulfilled or rejected.

Basic Promise syntax

const coursePromise = new Promise((resolve, reject) => {

    const courseAvailable = true;

    if (courseAvailable) {

        resolve("Course details loaded successfully");

    } else {

        reject(new Error("Course information is unavailable"));

    }

});

coursePromise

    .then((message) => {

        console.log(message);

    })

    .catch((error) => {

        console.error(error.message);

    })

    .finally(() => {

        console.log("Request completed");

    });

Output when the course is available:

Course details loaded successfully

Request completed

Here, resolve() fulfils the promise with a value, while reject() rejects it with a reason. The .then() method processes the successful result, and .catch() handles a rejection.

The finally() callback is useful for cleanup activities, such as hiding a loading indicator after a request finishes.

3. Understanding Promise States

A JavaScript promise has three possible states.

Pending

The promise is waiting for the asynchronous operation to finish.

Fulfilled

The operation has completed successfully, and the promise provides its result.

Rejected

The operation has failed, and the promise provides a rejection reason, commonly an error object.

Once a promise is fulfilled or rejected, it is considered settled. Its state cannot be changed again.

Understanding these states makes it easier to follow asynchronous operations and identify where error handling is required.

4. JavaScript Promise Examples

Example 1: Creating a Promise with setTimeout()

The following example simulates a server operation using a timer.

function getCourseDetails() {

    return new Promise((resolve) => {

        setTimeout(() => {

            resolve({

                id: 101,

                name: "JavaScript Development",

                duration: "3 Months"

            });

        }, 2000);

    });

}

getCourseDetails().then((course) => {

    console.log(course);

});

Expected output after approximately two seconds:

{

    id: 101,

    name: "JavaScript Development",

    duration: "3 Months"

}

The function returns a promise immediately. The promise is fulfilled after the timer completes.

Example 2: Promise chaining

Promise chaining is useful when one asynchronous operation depends on the result of another.

Imagine an application that first retrieves a learner's profile and then uses the learner ID to retrieve enrolled courses.

function getLearner() {

    return Promise.resolve({

        id: 25,

        name: "Arun"

    });

}

function getEnrolledCourses(learnerId) {

    return Promise.resolve([

        "JavaScript",

        "Python",

        "Web Development"

    ]);

}

getLearner()

    .then((learner) => {

        console.log("Learner:", learner.name);

        return getEnrolledCourses(learner.id);

    })

    .then((courses) => {

        console.log("Enrolled Courses:", courses);

    })

    .catch((error) => {

        console.error("Unable to load details:", error);

    });

The first .then() receives the learner information. It returns another promise, so the next .then() waits for the course information before executing.

Returning promises from .then() callbacks is important. It keeps the chain connected and allows later handlers to wait for earlier asynchronous work.

Example 3: Handling a rejected Promise

const paymentPromise = Promise.reject(

    new Error("Payment could not be processed")

);

paymentPromise

    .then((result) => {

        console.log(result);

    })

    .catch((error) => {

        console.error(error.message);

    });

Output:

Payment could not be processed

The promise is rejected, so the .catch() handler processes the error instead of the success handler.

5. What Is Async/Await in JavaScript?

async and await are JavaScript keywords that make promise-based code easier to read.

An async function always returns a promise. If it returns a normal value, JavaScript fulfils the returned promise with that value. If it throws an error, the returned promise is rejected.

The await keyword is used inside an async function, or at the top level of a JavaScript module. It waits for a promise to settle and provides the fulfilled value. If the promise is rejected, the rejection is thrown at the await expression.

Basic async function syntax

async function getMessage() {

    return "Welcome to JavaScript";

}

getMessage().then((message) => {

    console.log(message);

});

Although the function returns a string in its body, calling it produces a promise.

Basic await syntax

function getCourse() {

    return new Promise((resolve) => {

        setTimeout(() => {

            resolve("JavaScript Course Loaded");

        }, 2000);

    });

}

async function displayCourse() {

    const course = await getCourse();

    console.log(course);

}

displayCourse();

The await expression waits for the promise returned by getCourse() to fulfil before assigning its value to course.

It does not block the entire JavaScript application. It suspends the current async function while other JavaScript work can continue.

6. Async/Await with Practical Examples

Example 1: Fetch data from an API

A common use of asynchronous JavaScript is retrieving data from a backend API.

async function getUsers() {

    const response = await fetch(

        "https://jsonplaceholder.typicode.com/users"

    );

    if (!response.ok) {

        throw new Error(`HTTP error: ${response.status}`);

    }

    const users = await response.json();

    console.log(users);

}

getUsers();

This example uses a public demonstration API.

The process involves three main steps:

  • fetch() sends the HTTP request and returns a promise.
  • await response.json() reads and parses the response body asynchronously.
  • console.log(users) displays the retrieved data.

An important detail is that fetch() does not automatically reject its promise for HTTP error responses such as 404 or 500. Checking response.ok helps the application handle unsuccessful HTTP responses explicitly.

Example 2: Sequential asynchronous operations

Sometimes one operation needs the result of another before it can continue.

Consider retrieving a learner's profile and then using the profile ID to request course information.

async function loadLearnerDashboard() {

    const learner = await getLearner();

    const courses = await getEnrolledCourses(learner.id);

    console.log("Learner:", learner.name);

    console.log("Courses:", courses);

}

loadLearnerDashboard();

Here, the second request depends on the learner ID returned by the first operation. Therefore, awaiting them sequentially is appropriate.

Example 3: Returning data from an async function

async function calculateCourseFee() {

    const tuitionFee = 12000;

    const registrationFee = 1000;

    return tuitionFee + registrationFee;

}

async function displayFee() {

    const totalFee = await calculateCourseFee();

    console.log("Total Fee:", totalFee);

}

displayFee();

Output:

Total Fee: 13000

The returned value is wrapped in a fulfilled promise, and await retrieves the value inside the second async function.

7. JavaScript Error Handling with Async/Await

Asynchronous operations can fail for several reasons, including network problems, invalid API responses, unavailable services, and unexpected application errors.

JavaScript provides try, catch, and finally to handle these situations in async functions.

Using try...catch

async function loadCourses() {

    try {

        const response = await fetch(

            "https://jsonplaceholder.typicode.com/posts"

        );

        if (!response.ok) {

            throw new Error(`Request failed: ${response.status}`);

        }

        const courses = await response.json();

        console.log("Course data:", courses);

    } catch (error) {

        console.error("Unable to load data:", error.message);

    }

}

loadCourses();

The try block contains the asynchronous operations. If a promise awaited inside the block rejects, or an error is thrown, control moves to the catch block.

Using finally for cleanup

async function loadDashboard() {

    try {

        console.log("Loading dashboard...");

        const response = await fetch(

            "https://jsonplaceholder.typicode.com/users"

        );

        if (!response.ok) {

            throw new Error("Unable to retrieve user information");

        }

        const users = await response.json();

        console.log(users);

    } catch (error) {

        console.error(error.message);

    } finally {

        console.log("Loading process finished");

    }

}

loadDashboard();

The finally block runs after the try or catch path finishes. It is useful for resetting loading states, closing resources, or performing cleanup.

Handling errors at the function call

An async function returns a promise, so its caller can also handle errors using .catch().

async function getProfile() {

    throw new Error("Profile service unavailable");

}

getProfile().catch((error) => {

    console.error(error.message);

});

This approach is useful when the calling code is responsible for displaying errors or deciding what to do next.

Avoid silently swallowing errors. If an operation fails, either handle the failure meaningfully or allow it to propagate to a caller that can handle it.

8. Promise vs Async/Await

Promises and async/await are not competing technologies. Async/await is syntax built on top of promises.

Both approaches can handle asynchronous operations, but their coding styles differ.

Comparison

Promise Methods

  • Main syntax: .then(), .catch(), .finally()
  • Code structure: Uses promise chains and callbacks.
  • Error handling: Usually uses .catch().
  • Readability: Can be clear for short chains.
  • Return value: A promise is returned by promise-based operations.
  • Sequential tasks: Can be handled through chaining.
  • Concurrent tasks: Supports methods such as Promise.all().

Async/Await

  • Main syntax: async, await, try...catch
  • Code structure: Uses a more sequential-looking structure.
  • Error handling: Commonly uses try...catch.
  • Readability: Often easier for multi-step workflows.
  • Return value: Every async function returns a promise.
  • Sequential tasks: Can be written using multiple await expressions.
  • Concurrent tasks: Can also use Promise.all() with await.

The same operation using both approaches

Using Promises:

fetch("https://jsonplaceholder.typicode.com/users")

    .then((response) => {

        if (!response.ok) {

            throw new Error("Request failed");

        }

        return response.json();

    })

    .then((users) => {

        console.log(users);

    })

    .catch((error) => {

        console.error(error.message);

    });

Using Async/Await:

async function fetchUsers() {

    try {

        const response = await fetch(

            "https://jsonplaceholder.typicode.com/users"

        );

        if (!response.ok) {

            throw new Error("Request failed");

        }

        const users = await response.json();

        console.log(users);

    } catch (error) {

        console.error(error.message);

    }

}

fetchUsers();

Both examples perform the same basic task. The second version presents the steps in a more sequential style.

Which approach should you use?

Use promise chaining when it makes a short transformation or composition easy to understand. Use async/await when several operations need to be coordinated and a sequential structure improves readability.

9. Handling Multiple Asynchronous Operations

Modern applications often need information from several APIs. For example, a dashboard may retrieve course details, instructor information, and upcoming schedules.

If these requests do not depend on one another, running them sequentially can introduce unnecessary waiting.

JavaScript provides promise-combination methods to manage these situations.

Promise.all()

Promise.all() waits until every supplied promise fulfils. If one rejects, the returned promise rejects with that failure.

async function loadDashboard() {

    try {

        const [courses, instructors, schedules] = await Promise.all([

            fetch("https://jsonplaceholder.typicode.com/posts")

                .then((response) => {

                    if (!response.ok) {

                        throw new Error("Course request failed");

                    }

                    return response.json();

                }),

            fetch("https://jsonplaceholder.typicode.com/users")

                .then((response) => {

                    if (!response.ok) {

                        throw new Error("Instructor request failed");

                    }

                    return response.json();

                }),

            fetch("https://jsonplaceholder.typicode.com/todos")

                .then((response) => {

                    if (!response.ok) {

                        throw new Error("Schedule request failed");

                    }

                    return response.json();

                })

        ]);

        console.log({ courses, instructors, schedules });

    } catch (error) {

        console.error("Dashboard loading failed:", error.message);

    }

}

loadDashboard();

The requests are started without waiting for one another, and Promise.all() collects their results. The returned array preserves the order of the input promises, not the order in which they finish.

Promise.allSettled()

Use Promise.allSettled() when you need to know the outcome of every operation, even if some requests fail.

const results = await Promise.allSettled([

    fetch("/api/courses"),

    fetch("/api/instructors"),

    fetch("/api/schedules")

]);

results.forEach((result) => {

    if (result.status === "fulfilled") {

        console.log("Request completed:", result.value);

    } else {

        console.error("Request failed:", result.reason);

    }

});

This is useful for dashboards where one section can still be displayed even if another service is unavailable. In production code, also check each successful HTTP response's ok property because an HTTP error response still fulfils the fetch() promise.

Other Promise methods

  • Promise.all()
    • Behaviour: Fulfils when all promises fulfil; rejects if one rejects.
    • Useful When: Every operation must succeed.
  • Promise.allSettled()
    • Behaviour: Waits for all promises to settle.
    • Useful When: Every individual outcome is needed.
  • Promise.race()
    • Behaviour: Settles with the first input promise to settle.
    • Useful When: You need the first completed outcome, such as a timeout pattern.
  • Promise.any()
    • Behaviour: Fulfils with the first fulfilled promise; rejects if all inputs reject.
    • Useful When: Any one successful result is sufficient.

These methods help developers coordinate asynchronous tasks without creating deeply nested callbacks.

10. Common Mistakes in Asynchronous JavaScript

Forgetting to use await

Consider:

async function getCourses() {

    const courses = fetch("/api/courses");

    console.log(courses);

}

The variable courses contains a promise, not the parsed response data. The code needs to await the operation and process the response.

async function getCourses() {

    const response = await fetch("/api/courses");

    if (!response.ok) {

        throw new Error("Unable to retrieve courses");

    }

    const courses = await response.json();

    console.log(courses);

}

Using await unnecessarily in a loop

If tasks are independent, awaiting each one inside a loop can make the total process slower because each operation starts only after the previous one finishes.

Instead, start the operations together and use Promise.all() when all results are required.

Forgetting to return a promise in a chain

A promise started inside .then() should normally be returned if later steps depend on it. Otherwise, the outer chain may finish before the inner operation has completed.

Assuming fetch rejects for every HTTP error

A response with a 404 or 500 status does not automatically reject the promise returned by fetch(). Check response.ok and throw an error when the status is not acceptable for the application.

Ignoring rejected promises

An unhandled rejection can make failures difficult to diagnose. Add appropriate error handling at the operation or calling level, and avoid hiding errors that the application needs to respond to.

Using async without understanding what it returns

Every async function returns a promise, even when its body returns an ordinary value. Remember this when calling async functions from other parts of the application.

11. JavaScript Asynchronous Programming Best Practices

  • Use async/await for multi-step workflows. It can make dependent asynchronous operations easier to follow and maintain.
  • Return promises from promise-chain callbacks. This ensures that later steps wait for the asynchronous work to finish.
  • Handle errors at the right level. Use try...catch or .catch() where the application can respond meaningfully to a failure.
  • Check HTTP response status. Validate response.ok when using fetch() so unsuccessful HTTP responses are not treated as successful application results.
  • Use Promise.all() for independent operations. Start unrelated requests together when the application needs all their results.
  • Use Promise.allSettled() when partial results are useful. This allows the application to inspect successful and failed outcomes individually.
  • Avoid unnecessary sequential waiting. Only await operations one by one when a later task depends on an earlier result or when sequencing is required.
  • Keep loading and error states visible. Give users appropriate feedback while requests are running or when they fail.
  • Use timeouts and cancellation where appropriate. For operations that should not continue indefinitely, consider request cancellation with AbortController.
  • Keep asynchronous functions focused. Separate API communication, data transformation, and interface updates when that improves code organisation.

Conclusion

JavaScript Promises and async/await are essential for handling asynchronous operations in modern web development. Promises provide a structured way to manage future results and failures, while async/await makes promise-based workflows easier to read and maintain.

Start by understanding promise states and the purpose of .then() and .catch(). Then practise writing async functions, handling API responses, managing errors, and combining independent requests with Promise.all().

For learners developing JavaScript and web development skills, Well Spring Talent Solutions can be explored as a learning option. Practising these concepts through real API integrations, dashboard features, and interactive web applications will help build a stronger understanding of how modern JavaScript applications work.

Frequently Asked Questions

A Promise is an object that represents the eventual completion or failure of an asynchronous operation. It provides methods such as .then(), .catch(), and .finally() to handle results and errors.

Async/await is syntax for working with promises in a more readable way. The async keyword declares an asynchronous function that returns a promise, while await waits for an awaited promise to settle and provides its fulfilled value.

Promises are the underlying mechanism for representing asynchronous results. Async/await is a syntax built on promises that makes multi-step asynchronous code easier to read. Both support error handling and concurrent operations.

In JavaScript modules, top-level await is supported. In a traditional non-module script, await generally needs to be inside an async function. Browser and runtime support depends on the environment.

Promise.all() combines multiple promises and fulfils when all of them fulfil. If one input promise rejects, the combined promise rejects. It is useful when independent asynchronous operations must all succeed before the application continues.

Use try...catch around awaited operations. The try block contains the asynchronous work, and the catch block handles rejected promises or thrown errors. A finally block can be used for cleanup.

Neither is universally better. Async/await is often easier to read for multi-step workflows, while promise chaining can be concise for simple transformations and composition. Choose the style that makes the control flow and error handling easiest to understand.

Still Have Questions?

If you didn't find your answer here, feel free to reach out to us.

Contact

Your Local Destination for IT Training

We provide IT training, internships, and online learning for students in Trichy and across India.

IT Training Areas in Trichy

Thillai Nagar KK Nagar Srirangam Cantonment Woraiyur Tennur Karumandapam Edamalaipatti Pudur Puthur Crawford BHEL Township Ariyamangalam Kattur Ponmalai (Golden Rock) Vayalur Road Bharathidasan Salai Chathiram Bus Stand Palakkarai Subramaniapuram Sangiliyandapuram Trichy Airport Area Thiruverumbur Samayapuram Manachanallur Panjappur Navalpattu Central Bus Stand Trichy Railway Junction Gandhi Market Palpannai Anna Nagar Thiruvanaikoil TVS Tollgate Ponnagar Beemanagar Kallukuzhi Thuvakudi Pudukottai Road Thanjavur Road Karur Bypass Road

Online IT Training Across India

Can't attend classes in person? Explore online IT training opportunities with Well Spring Talent Solutions, designed to help learners develop practical technology skills from different locations across India.

Join Our WhatsApp Channel ๐Ÿ’ฌ

Join our WhatsApp community for internship updates, IT courses, placement support, and latest job opportunities.

Join Now

Our Branches

Visit our branches in Tiruchirappalli for IT training and career guidance.