Understanding Asynchronous JavaScript with async and await
2026-06-1
Understanding Asynchronous JavaScript with async and await
JavaScript often needs to wait for work that does not finish immediately. An API request takes time, a database must return data, and a timer completes only after its delay.
If JavaScript stopped the entire application while waiting, the interface would feel frozen. Asynchronous code allows other work to continue until the result is ready.
Synchronous and asynchronous code
Synchronous statements run one after another:
console.log("First");
console.log("Second");
console.log("Third");
The output is predictable:
First
Second
Third
A timer behaves differently:
console.log("First");
setTimeout(() => {
console.log("Second");
}, 1000);
console.log("Third");
The output becomes:
First
Third
Second
JavaScript schedules the callback and continues running the remaining code.
What is a Promise?
A Promise represents a result that may become available later. It can be in one of three states:
pending— the operation is still running;fulfilled— the operation completed successfully;rejected— the operation failed.
The Fetch API returns a Promise:
fetch("https://api.example.com/users")
.then((response) => response.json())
.then((users) => {
console.log(users);
})
.catch((error) => {
console.error(error);
});
This works, but several chained operations can become difficult to read.
Using async and await
An async function always returns a Promise. Inside it, await pauses that
function until a Promise settles:
async function getUsers() {
const response = await fetch("https://api.example.com/users");
const users = await response.json();
return users;
}
The code reads from top to bottom, which makes the sequence easier to follow. Only the current async function pauses; JavaScript can continue handling other work.
Handling errors with try and catch
Network requests can fail, so asynchronous code should handle errors:
async function getUsers() {
try {
const response = await fetch("https://api.example.com/users");
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Could not load users:", error);
return [];
}
}
An important detail is that fetch does not reject its Promise for every HTTP
error. A 404 or 500 response still needs to be detected with
response.ok.
Running independent requests together
This code waits for one request before starting the next:
const users = await fetchUsers();
const posts = await fetchPosts();
If the requests do not depend on each other, start them together:
const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]);
Promise.all is usually faster for independent tasks because both operations
run at the same time. It rejects if any included Promise rejects.
Sequential work is sometimes correct
Parallel execution is not always appropriate. The second operation may need the result of the first:
const user = await createUser(formData);
const profile = await createProfile(user.id);
Here, createProfile cannot begin until createUser provides the user ID.
A common mistake with array methods
Using await inside forEach does not make forEach wait:
items.forEach(async (item) => {
await saveItem(item);
});
console.log("Finished");
Finished may appear before the items are saved.
For sequential processing, use for...of:
for (const item of items) {
await saveItem(item);
}
console.log("Finished");
For parallel processing, map the items to Promises:
await Promise.all(items.map((item) => saveItem(item)));
console.log("Finished");
Do not forget to await the result
Calling an async function without await gives you a Promise, not its final
value:
async function getName() {
return "Anand";
}
const name = getName();
console.log(name);
The console displays a Promise. Use:
const name = await getName();
This must run inside another async function or in an environment that supports top-level await.
An example from a URL shortener
A frontend can submit a long URL and wait for the shortened result:
async function shortenUrl(url) {
const response = await fetch("/api/shorten", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ url }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "Could not shorten the URL");
}
return data.shortUrl;
}
The UI can use try, catch, and finally to manage loading and error states:
try {
setIsLoading(true);
const shortUrl = await shortenUrl(longUrl);
setShortUrl(shortUrl);
} catch (error) {
setError(error.message);
} finally {
setIsLoading(false);
}
Practical rules
- Use
asyncandawaitwhen they make the sequence easier to read. - Check
response.okwhen usingfetch. - Wrap operations that can fail in
tryandcatch. - Use
Promise.allfor independent tasks. - Use
for...ofwhen operations must run sequentially. - Avoid combining
forEachwith async callbacks. - Always decide what the interface should show while a request is running.
Conclusion
Asynchronous JavaScript becomes easier once you stop thinking of await as
freezing the whole application. It pauses only the current async function while
the runtime continues processing other work.
Promises describe future results, and async and await provide a readable
way to work with them. The most important skills are choosing between parallel
and sequential execution, handling failures, and keeping the user informed
while an operation is in progress.