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.