Reading Time 7
Number of Words 1445
JavaScript is single-threaded, which means if you execute a long-running operation (like network requests, file I/O) synchronously, you’ll block the event loop and make the UI or other code unresponsive.
Asynchronous programming is essential to keep applications responsive. In the past you used callbacks, then Promises, and now the async/await syntax offers a cleaner, more readable way to write asynchronous code.
The keywords async and await allow you to structure asynchronous operations in a style that resembles synchronous code — improving readability and maintainability — while still being non-blocking under the hood.
async functions
When you prepend a function definition with the async keyword, you mark it as asynchronous. This has a few important implications:
- An
asyncfunction always returns a Promise, even if you return a plain value. The engine implicitly wraps non-Promise return values intoPromise.resolve(value). - Inside an
asyncfunction, you may use theawaitkeyword (see below) to pause execution until a Promise resolves (or rejects). - If the function throws an exception, the returned Promise is rejected with that exception.
Syntax
async function myFunction(param1, param2) {
// body
return someValue;
}
Or using an arrow function:
const myFunction = async (param) => {
// body
return someValue;
};
Example
async function getNumber() {
return 42;
}
getNumber().then(value => {
console.log(value); // 42
});
Even though we returned 42 (a non-Promise), the function implicitly returns a Promise resolved with 42.
Key points
- You don’t need to explicitly write
return Promise.resolve(value)— the language takes care of it. - If you explicitly return a Promise (for example via
fetch(...)or other async operations), the function will return that Promise. - If you call an
asyncfunction withoutawait, it still returns a Promise immediately; you can chain.then()or use anotherawait.
await keyword
The await keyword can only appear inside an async function (or at the top-level in modules in environments that support top-level await). await takes a Promise (or thenable) and pauses execution within the async function until the Promise settles (resolves or rejects). The result of the await expression is the resolved value, or if the Promise rejects, the exception is propagated.
Syntax
let result = await somePromise;
Example
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function example() {
console.log("Start");
await delay(1000);
console.log("After 1 second");
}
example();
console.log("This runs while waiting");
Output:
Start
This runs while waiting
After 1 second
This demonstrates that while the async function is “awaiting”, the rest of the program (outside the await) is not blocked.
Key points
- You cannot use
awaitin a non-async function (unless your environment supports top-level await). - After the
await, code continues only when the awaited Promise settles. - While the
asyncfunction is paused at anawait, the JavaScript event loop is free to handle other tasks (UI events, other async operations). - Because of
await, asynchronous code reads sequentially, like synchronous code, which reduces the “callback hell” or deeply nested.then()chains.
Practical patterns and usage
Chaining multiple asynchronous steps
Instead of:
fetch('/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(githubResponse => githubResponse.json())
.then(githubUser => {
// use githubUser
})
.catch(error => {
console.error(error);
});
You can write:
async function showAvatar() {
try {
const response = await fetch('/user.json');
const user = await response.json();
const githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
const githubUser = await githubResponse.json();
const img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "avatar";
document.body.append(img);
await new Promise(resolve => setTimeout(resolve, 3000));
img.remove();
} catch (error) {
console.error('Failed to load:', error);
}
}
showAvatar();
Here the code flows top-to-bottom, making asynchronous steps clear. The use of try/catch enables error handling in a synchronous style.
Parallel asynchronous operations
If you have multiple independent asynchronous tasks, you may want to launch them concurrently rather than awaiting one then the next. Example:
async function loadBoth() {
const promise1 = fetch('/data1.json');
const promise2 = fetch('/data2.json');
const [response1, response2] = await Promise.all([promise1, promise2]);
const data1 = await response1.json();
const data2 = await response2.json();
// use data1 and data2
}
Here by starting both fetch() calls before awaiting, you let them run in parallel. Then you wait for both to finish with Promise.all. This pattern is efficient when tasks don’t depend on each other.
Error handling
Using async/await, you can handle errors with try/catch, which is more familiar than the .catch() syntax on Promises:
async function fetchData() {
try {
const response = await fetch('/endpoint');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching data:', error);
throw error; // re-throw if you want callers to know
}
}
Returning values from async functions
Since async functions always return a Promise:
async function getData() {
const data = await fetch('/item').then(res => res.json());
return data;
}
getData().then(myData => {
// use myData
});
If you return data, the caller receives a Promise that resolves to data. You cannot return the raw value in a synchronous style — you must either await the call or use .then().
Top-level await (modern environments)
In modern JavaScript modules (ESM) or environments supporting top-level await, you can use await outside of functions:
// main.mjs
const response = await fetch('/config.json');
const config = await response.json();
// proceed using config
This lets you avoid wrapping everything in an async function when starting your program.
Common pitfalls & best practices
- Forgetting
async: If you mark a functionasyncbut never useawait, it still returns a Promise — but you might inadvertently ignore that Promise, leading to unexpected behavior. - Blocking the whole application: Even though the code “looks” sequential,
awaitonly pauses theasyncfunction — the JavaScript event loop is not blocked. But if youawaita long-running synchronous operation (rare), you can still cause perceived blocking. - Sequential vs parallel: If you
awaitmultiple independent tasks in sequence, you may slow your program unnecessarily. UsePromise.all()or start tasks before awaiting. - Error handling: Don’t forget to use
try/catchinsideasyncfunctions (or handle rejection on the returned Promise) — unhandled Promise rejections can cause silent failures. - Return values: Remember that callers must handle the returned Promise (via
awaitor.then()). You cannot treat results fromasyncfunctions as synchronous return values. - Avoid mixing too many nested async calls: While
async/awaitgreatly simplifies syntax, you can still end up with deeply nested structures. Try to keep functions focused and small. - Use meaningful names: Mark functions
asynconly when they perform asynchronous operations — makes API semantics clearer to readers.
Why use async/await?
- Readability: Asynchronous code written with
awaitresembles synchronous code, making it easier to follow. - Maintainability: Fewer nested callbacks and less chaining reduces complexity.
- Error-handling: Using
try/catchinstead of.then().catch()is more intuitive. - Control flow: You can pause execution within an
asyncfunction, then resume; the rest of the application remains responsive.
Under the hood – what happens?
When you mark a function as async, the JavaScript engine transforms the function such that its execution always returns a Promise. Each await expression effectively splits the function into multiple steps, with the engine scheduling continuation via micro-tasks (Promise resolution) rather than blocking the thread.
For example:
async function foo() {
console.log(1);
const result = await somePromise;
console.log(result);
return result * 2;
}
Internally, it's akin to:
function foo() {
console.log(1);
return somePromise.then(result => {
console.log(result);
return result * 2;
});
}
But async/await lets you write it in a more straightforward linear style.
When not to use async/await
- If you don’t need to wait for a promise result right away (for example, you can fire off tasks and ignore completion).
- If you’re doing many independent quick tasks and want full parallelism, you may rely more directly on
Promise.all,Promise.raceand manage promises manually. - In very performance-critical code where micro-optimizations matter: the overhead of
asyncfunctions andawaitmay be slightly higher than raw promise chaining, though in most real-world applications this cost is negligible.
Summary
The async/await syntax represents a major improvement in how JavaScript handles asynchronous operations. By marking functions with async and pausing them with await, you can write code that is non-blocking, clear, and easy to maintain.
Key take-aways:
asyncfunctions always return a Promise.awaitcan only be used insideasyncfunctions (or at top-level in supported modules).- The code after
awaitwaits for the Promise to resolve, but other code (outside the function) continues running. - Use
try/catchfor error handling inasyncfunctions. - To run tasks in parallel, start them before awaiting or use
Promise.all. - For best readability and maintainability, keep
asyncfunctions focused (one main responsibility), avoid deep nesting, and name accordingly.
Once you’re familiar with async/await, you’ll rarely need to fall back on raw .then() chaining — and your asynchronous code will be easier to read, write, and debug.