Async Await in JavaScript is a modern syntax built on top of promises that makes asynchronous code easier to read and write. Instead of chaining many `then` and `catch` handlers, developers can express asynchronous steps in a style that looks closer to normal sequential logic. This does not make the code truly synchronous. It makes the structure of asynchronous flow easier for humans to follow.
The real value of async and await is readability. Many browser and platform APIs return promises, which means modern JavaScript developers constantly work with future results. Async and await provide a cleaner way to consume those promises while still preserving non-blocking behavior. This is why they became one of the most important additions to the language for practical application development.
Why async and await matter
Before async and await, promise chains already improved asynchronous code compared with deep callback nesting. Even so, long chains could still become hard to scan when several dependent steps were involved. Async and await reduce that friction by letting the code describe the sequence more directly: start an asynchronous operation, wait for its result, handle the value, and continue. The logic stays asynchronous underneath, but the shape becomes easier to reason about.
This is especially useful in real applications where data loading, parsing, validation, UI updates, and error handling often happen together. The better the control flow reads, the easier it is to debug, extend, and maintain. That is why async and await are so widely used in modern JavaScript projects.
The async keyword
The `async` keyword is placed before a function declaration or function expression to mark that function as asynchronous. An async function always returns a promise. Even if it appears to return a plain value, JavaScript wraps that value in a fulfilled promise automatically. This is one of the first important rules to understand when working with async code.
async function getStatus() {
return "ready";
}
getStatus().then(function (value) {
console.log(value);
});
In this example, `getStatus` appears to return a plain string, but from the outside it still behaves as a promise returning function. This is why async functions fit naturally into promise based workflows.
The await keyword
The `await` keyword pauses the execution of an async function until the awaited promise settles. If the promise is fulfilled, `await` gives back the resolved value. If the promise is rejected, `await` throws the rejection reason like an error. This makes asynchronous code feel closer to normal variable assignment and try/catch control flow.
function fetchValue() {
return Promise.resolve(42);
}
async function showValue() {
const result = await fetchValue();
console.log(result);
}
showValue();
The key idea is that `await` only works inside an async context in normal usage. It does not freeze the whole browser. It only pauses that async function while allowing the rest of the JavaScript environment to continue handling other work.
Async and await still use promises underneath
It is easy to think async and await replaced promises completely, but they did not. They are syntax built on top of promises. Every awaited value is expected to be promise like, and every async function itself returns a promise. This matters because understanding promise behavior still helps you reason about timing, error flow, and composition even when the syntax looks simpler.
That is why learning promises before or alongside async and await is so helpful. Async and await improve readability, but the underlying asynchronous model remains promise based. The better you understand that model, the easier it is to debug more advanced flows.
Handling errors with try and catch
One of the biggest advantages of async and await is that rejected promises can be handled using `try` and `catch`. This feels natural because it matches ordinary error handling patterns in JavaScript. Instead of attaching a separate `catch` to each promise chain segment, the code can group several awaited steps inside one error handling block.
async function loadData() {
try {
const result = await Promise.reject("Request failed");
console.log(result);
} catch (error) {
console.log(error);
}
}
loadData();
This style usually improves readability when several asynchronous operations belong to one larger task. It keeps success logic and failure logic more clearly separated and easier to scan.
Sequential versus parallel awaits
Awaiting one promise after another creates sequential flow. This is correct when each step depends on the previous result. But if two asynchronous tasks are independent, waiting for the first to finish before starting the second may waste time. In those cases, the promises can often be started together and then awaited as a group. The important skill is knowing whether the task relationship is dependent or independent.
This is a good example of why async and await improve syntax but do not remove the need for careful design. The code may look neat while still being slower than necessary if the promise scheduling strategy is poor. Readable code and good asynchronous structure should work together.
Returning values from async functions
An async function can return a final value the same way an ordinary function does, but the caller receives that value through a promise. This is what makes async functions composable. One async function can await another, and the outer function can continue working with the resolved result as part of a bigger workflow.
async function getUserRole() {
return "admin";
}
async function showRole() {
const role = await getUserRole();
console.log(role);
}
showRole();
This composition style is part of what makes async functions elegant. They can read like normal procedural code while still preserving the promise based communication between layers of the application.
Common use cases
Async and await are used heavily with network requests, database calls, file operations, authentication flows, delayed UI steps, and any other operation whose result arrives later. In browser work, they are especially common with the Fetch API. A request is made, the response is awaited, the body is parsed, and the interface is updated. This sequence is one of the classic examples that show why async and await became so popular.
The readability benefit becomes stronger as the flow becomes more realistic. A chain of loading, validating, transforming, and rendering data is much easier to understand when expressed in a clear async function than when scattered through deeply nested continuation logic.
Common mistakes with async and await
- Forgetting that async functions always return promises.
- Using await outside an allowed async context in normal scripts.
- Assuming await blocks the whole browser instead of only pausing the async function.
- Running dependent and independent asynchronous tasks with the same scheduling strategy.
- Ignoring try/catch and leaving rejected promises unhandled.
Best practices for async and await
Use async and await when they genuinely improve the readability of asynchronous flow. Keep related awaited steps together, use `try` and `catch` when rejection must be handled locally, and think carefully about whether tasks should run sequentially or in parallel. Async syntax makes the code easier to read, but good asynchronous design still depends on the developer’s judgment.
Async and await became central in JavaScript because they let developers write modern asynchronous code in a more maintainable form. Once you understand how they relate to promises, how errors move through them, and how they shape control flow, a large part of modern browser and server side JavaScript becomes much easier to understand.
FAQ
What does async do in JavaScript?
Async marks a function as asynchronous and makes it return a promise automatically.
What does await do?
Await pauses the current async function until a promise settles and then gives back the fulfilled value or throws the rejection reason.
Does await make code synchronous?
No. It makes the function read more sequentially, but the underlying behavior is still asynchronous and promise based.
Practical Patterns with Async and Await
One of the best ways to improve async await code is to think in layers. The first layer is the user-facing action, such as clicking a button, loading a profile, or submitting a form. The second layer is the service call that talks to an API or reads data. The third layer is error recovery. When these layers stay separate, async code becomes easier to read and much easier to debug. The event handler can focus on what the user is doing, the async function can focus on data flow, and the catch block can focus on failure states.
Another practical pattern is to avoid mixing too many unrelated awaits inside one long function. A long sequence of await statements may work, but it often hides the real steps of the operation. It is usually better to extract smaller async functions like loadUser, loadOrders, and loadRecommendations, then await those from a parent workflow. This keeps naming clear and creates better reuse. It also makes it easier to test each part independently because each async function has a single responsibility instead of handling every detail itself.
Performance also matters. Async await improves readability, but it does not automatically make code faster. If two operations do not depend on each other, starting them together and then awaiting both can reduce total waiting time. If an operation depends on earlier data, it should stay sequential. Good async design is not about using await everywhere. It is about understanding when work should happen one step at a time and when it can happen in parallel without creating incorrect behavior.
Should every promise-based function be rewritten with async await?
Not always. Async await is excellent for readability, but small utility chains can still be clear with plain promises when the flow is simple.
When async await improves design
Async await is most valuable when the code represents a real workflow with multiple dependent steps. Loading data, validating it, transforming it, and then updating the interface are easier to follow when each step is written in order. This makes maintenance easier because the function reads like an actual process instead of a scattered chain of delayed callbacks.