JavaScript Async Await Explained in 2026: Promises Made Simple
Learn JavaScript async and await with practical examples covering promises, API requests, error handling, and parallel execution.
JavaScript can request data, read files, or wait for a timer without freezing everything else. The difficult part for beginners is understanding when a result becomes available and how to use it safely. This guide explains callbacks, promises, async and await, error handling, and parallel execution with practical examples you can run in a browser or Node.js in 2026.
JavaScript Async Await Explained: The Short Answer
JavaScript async and await provide a readable way to work with promises. An async function always returns a promise, while await pauses that function’s execution until a value becomes available or a promise rejects. Other JavaScript tasks can continue while the operation is pending. Together, they simplify asynchronous code, error handling, and operations such as fetching API data.
Why Does JavaScript Need Asynchronous Code?
JavaScript commonly runs application code on a single main thread. If every operation blocked that thread until completion, interfaces could become unresponsive whenever an application requested information from a server.
Asynchronous APIs let JavaScript start an operation and continue executing other code while waiting for its result. When the operation finishes, JavaScript processes the relevant continuation through its event loop.
Consider this example:
console.log(“Start”);
setTimeout(() => console.log(“Timer finished”), 1000);
console.log(“End”);
The output is Start, End, and then Timer finished approximately one second later. The timer does not block the statement after it.
Important: Asynchronous code does not automatically make CPU-intensive calculations run in parallel. A long synchronous loop can still block the main thread. Browser Web Workers and Node.js worker threads address different kinds of workloads.
From Callbacks to Promises: Understanding the Progression
Callbacks: The Original Pattern
A callback is a function passed into another function to run later. Timers, event listeners, and older Node.js APIs commonly use callbacks.
setTimeout(() => {
console.log(“Task complete”);
}, 1000);
Callbacks are useful, but nested dependent operations can become difficult to read. Each additional operation may require another nested function, especially when handling errors.
Promises: Representing Future Results
A promise represents the eventual completion or failure of an asynchronous operation. It can be pending, fulfilled, or rejected.
const task = new Promise((resolve) => {
setTimeout(() => resolve(“Done”), 1000);
});task.then((result) => console.log(result));
Here, resolve fulfills the promise with the string Done. The then method registers a callback that receives the fulfilled value.
Promises also support catch for rejection handling and finally for cleanup. Chaining these methods avoids some nesting, although longer chains can become harder to follow.
Async and Await: Writing Promise-Based Code Clearly
The same promise can be consumed using await inside an async function:
async function runTask() {
const result = await task;
console.log(result);
}runTask();
The code reads from top to bottom. JavaScript suspends the execution of runTask while the promise is pending, without blocking the entire application.
Callbacks vs Promises vs Async Await
| Feature | Callbacks | Promises | Async/Await |
|---|---|---|---|
| Basic approach | Pass a function | Chain then methods | Use async functions and await |
| Readability | Good for simple events | Good for reusable chains | Good for sequential logic |
| Error handling | Callback conventions | catch method | try/catch |
| Return behavior | Depends on API | Promise | Promise |
| Best use | Events and legacy APIs | Promise composition | Most modern asynchronous workflows |
These approaches are not competing runtime systems. Async and await work directly with promises, while callbacks remain important for event handlers and other APIs.
According to MDN’s async function documentation, async functions have been widely supported across browsers since April 2017. They are established JavaScript features, not experimental syntax introduced in 2026.
How Async and Await Actually Work
Start with an async function that returns a regular value:
async function getNumber() {
return 42;
}
Calling getNumber() does not directly return 42. It returns a promise fulfilled with 42.
getNumber().then((value) => console.log(value));
The async keyword ensures that the function’s return value is represented by a promise.
Now consider an awaited promise:
async function example() {
console.log(“Before”);
await Promise.resolve();
console.log(“After”);
}example();
console.log(“Outside”);
The output is Before, Outside, After. Even when the promise is already fulfilled, execution after await continues asynchronously.
Key distinction: Await suspends the surrounding async function, not the entire JavaScript thread. Code outside that function can continue.
Await also works with ordinary values, but using it on a value that does not represent asynchronous work is usually unnecessary.
Practical Example: Fetching Data From an API
One of the most useful applications of async and await is requesting information through the Fetch API. If you need the underlying concepts first, read what is an API.
This example requests a public sample post:
async function loadPost() {
const response = await fetch(“https://jsonplaceholder.typicode.com/posts/1”);
if (!response.ok) {
throw new Error(“Request failed”);
}
const post = await response.json();
console.log(post.title);
}loadPost();
The first await waits for the response headers. The second waits for the response body to be read and parsed as JSON.
These are separate asynchronous operations. The fetch function returns a promise, and response.json() also returns a promise.
Checking response.ok matters because Fetch normally does not reject simply because the server returns an HTTP error such as 404 or 500.
For production applications, combine this pattern with error handling, request cancellation when appropriate, and user-visible loading states.
Handling Errors With Try and Catch
A promise can reject because of a network failure, invalid operation, or an error thrown inside asynchronous code.
Try/catch provides familiar control flow for handling these failures:
async function loadUser() {
try {
const response = await fetch(“/api/user”);
if (!response.ok) throw new Error(“HTTP ” + response.status);
return await response.json();
} catch (error) {
console.error(“Loading failed:”, error);
throw error;
}
}
The catch block handles rejected awaited promises and synchronous exceptions thrown within the try block. Rethrowing the error allows the calling function to decide what should happen next.
A finally block can perform cleanup whether the operation succeeds or fails:
try {
await saveChanges();
} finally {
hideLoadingIndicator();
}
Use finally for cleanup such as removing loading indicators or releasing resources. Avoid displaying success messages in finally because it executes after failures too.
Also remember that try/catch does not automatically handle a promise that you start but never await or return.
Running Independent Tasks With Promise.all
A frequent performance mistake is awaiting independent operations one after another.
const users = await fetchUsers();
const products = await fetchProducts();
This approach waits for fetchUsers to finish before starting fetchProducts. That is appropriate when the second operation depends on the first, but unnecessary when both requests are independent.
Use Promise.all to start both operations together:
const [users, products] = await Promise.all([
fetchUsers(),
fetchProducts()
]);
Both operations begin before the combined promise is awaited. Their results appear in input order, even if the underlying operations finish in a different order.
MDN documents that Promise.all rejects when any input promise rejects. However, rejection does not automatically cancel other operations that have already started.
When to Use Promise.allSettled
Sometimes you want every result, including failures. For example, a dashboard might load several independent widgets and display whichever ones succeed.
const results = await Promise.allSettled([
fetchUsers(),
fetchProducts()
]);
Each result contains a status of fulfilled or rejected, along with its value or rejection reason.
Choose Promise.all when every result is required. Choose Promise.allSettled when partial success is acceptable. Neither method guarantees that network requests complete simultaneously.
Five Common Async Await Mistakes
- Forgetting await: Assigning an unresolved promise instead of the desired result. Check whether the function returns a promise before accessing its value.
- Using forEach with async callbacks: Array.forEach does not await returned promises. Use for…of for sequential operations or Promise.all with map for concurrent work.
- Awaiting independent operations sequentially: This increases total waiting time when the operations could run together.
- Ignoring rejected promises: Always handle expected failures through try/catch, a returned promise, or a catch handler.
- Assuming await makes synchronous code nonblocking: Expensive calculations still block a JavaScript thread unless moved to another execution context.
For example, this does not wait for each operation before continuing:
items.forEach(async (item) => {
await saveItem(item);
});
If processing must be sequential, use:
for (const item of items) {
await saveItem(item);
}
If operations are independent and concurrency is safe, use Promise.all with items.map instead. For large collections, consider limiting concurrency to avoid overwhelming a server.
Practice Async Await in Node.js in 2026
You can practice these examples directly in modern browser developer tools or Node.js without adding a framework.
As of October 26, 2026, the official Node.js release schedule lists Node.js 26 as Current and Node.js 24 as an LTS release. For stable learning and deployment environments, a supported LTS version is a sensible choice.
To inspect your installed runtime, open Terminal, PowerShell, or Command Prompt and run:
node –version
npm –version
Create a file called demo.mjs:
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function main() {
console.log(“Starting”);
await delay(1000);
console.log(“Finished”);
}main().catch(console.error);
Run it with:
node demo.mjs
Next, replace the single awaited delay with Promise.all containing multiple delays. Observe that independent timers overlap rather than waiting sequentially.
You can also use top-level await inside an ECMAScript module. The .mjs extension enables this module format in Node.js without additional package configuration.
If you are deciding which language to learn alongside JavaScript, see Python vs JavaScript. Developers moving toward larger front-end projects may also benefit from TypeScript vs JavaScript.
FAQ: JavaScript Async Await Explained
Is async await better than promises?
Async and await are built on promises, so they are not replacements for them. Await generally improves readability when several dependent operations must happen in sequence. Promise methods remain useful for combining independent operations, constructing reusable workflows, and handling results. Most modern JavaScript applications use both approaches depending on the situation.
Can I use await without async?
Normally, await belongs inside an async function. However, modern JavaScript also supports top-level await within ECMAScript modules. You cannot freely use await at the top level of an ordinary classic script. In Node.js, an .mjs file supports module syntax, including top-level await, without requiring additional package configuration.
Does await block the JavaScript thread?
No. Await suspends the current async function until its awaited value becomes available. Other scheduled JavaScript work can continue while the promise is pending. However, synchronous calculations performed before or after await still execute normally and can block the main thread. Await should not be confused with multithreading or CPU parallelism.
What happens when an awaited promise rejects?
A rejected promise causes await to throw the rejection reason within the surrounding async function. A try/catch block can handle it. If the function does not catch the error, its returned promise rejects. The caller must then handle that rejection, either by awaiting it inside another try/catch or attaching a catch handler.
When should I use Promise.all instead of multiple await statements?
Use Promise.all when several asynchronous operations are independent and you need all their successful results. It starts those operations without unnecessarily waiting for earlier ones to finish. Use sequential await statements when later operations depend on earlier results. Remember that Promise.all rejects if any input rejects and does not cancel unfinished work automatically.
Conclusion
Understanding async and await starts with understanding promises. Callbacks provide a foundation, promises represent future results, and async functions make consuming those results easier to read and maintain. Await helps express dependent operations clearly, while Promise.all allows independent work to proceed concurrently. Reliable asynchronous code also checks HTTP responses, handles rejected promises, and avoids unnecessary blocking operations.
Begin with a timer, move to a fetch request, and then combine multiple requests. Run each example, inspect its output, and deliberately trigger an error. That practical progression will prepare you to work with asynchronous JavaScript in real applications.



