I use async and await because they make asynchronous JavaScript readable. Not magical. Not faster by default. Just easier to follow when a request, a database call, or a WordPress REST API endpoint needs time to answer.
The way I think about it is simple: Promises are still doing the work underneath. async/await just lets me write that Promise-based code in the order I actually think about it.
What async Does
When you mark a function as async, that function always returns a Promise. Even if you return a plain value, JavaScript wraps it for you.
async function getNumber() {
return 42;
}
getNumber().then(function (number) {
console.log(number); // 42
});If an async function throws an error, the returned Promise rejects. That is the first important mental shift: once a function is async, callers need to treat it like a Promise-returning function.
An async function declaration creates a function that returns a new Promise, resolving with the function’s return value or rejecting with an uncaught exception. MDN Web Docs
What await Really Does
await pauses the current async function until the Promise settles. It does not freeze the browser. It does not block the whole JavaScript thread. The rest of the page can keep working.
console.log('1');
async function run() {
console.log('2');
const value = await Promise.resolve('done');
console.log('4', value);
}
run();
console.log('3');The output is 1, 2, 3, 4 done. The function pauses at await, returns control to the rest of the program, and continues later.
That is the part I wish more developers understood early. await makes the code look linear, but the runtime is still asynchronous.
Why I Prefer try/catch
The biggest practical win is error handling. Promise chains can be perfectly valid, but once a flow has several steps, I find try/catch much easier to read and debug.
async function loadDashboard(userId) {
try {
const user = await fetchUser(userId);
const orders = await fetchOrders(user.id);
return { user, orders };
} catch (err) {
console.error('Dashboard failed', err);
return { user: null, orders: [] };
}
}This reads like the flow I have in my head: get the user, get the orders, return the result, handle anything that failed. For more detail on the mechanics, my try…catch guide covers the error-handling side separately.
catch block makes async bugs much harder to find. Log the error, recover from it, or throw it again.The Performance Trap
The one async/await mistake I see most often is accidental sequential code: three independent requests written as three separate await lines. It looks clean, but it makes the browser wait longer than needed.
I moved that topic into a shorter, focused post with live demos: Sequential vs Parallel JavaScript: The Performance Mistake.
The short version: if one async operation needs the result of the previous one, keep it sequential. If the operations are independent, start them together with Promise.all().
The forEach Trap
This is the other mistake worth calling out because it looks so innocent:
async function saveAll(items) {
items.forEach(async function (item) {
await save(item);
});
console.log('done?');
}forEach does not wait for the async callback. It ignores the Promise returned by that callback, so done? can run before the saves finish.
Use for...of when the work must happen one item at a time:
async function saveAll(items) {
for (const item of items) {
await save(item);
}
console.log('done');
}Use Promise.all() with map() when the items can be processed together:
async function saveAll(items) {
await Promise.all(items.map(function (item) {
return save(item);
}));
console.log('done');
}A Real WordPress Shape
In WordPress frontend code, I usually see async/await around the REST API. A common pattern is loading a post, then loading data that depends on that post.
async function loadPostBundle(postId) {
const base = '/wp-json/wp/v2';
try {
const post = await fetch(base + '/posts/' + postId)
.then(function (response) { return response.json(); });
const author = await fetch(base + '/users/' + post.author)
.then(function (response) { return response.json(); });
return { post, author };
} catch (err) {
console.error('Could not load post bundle', err);
return null;
}
}This is sequential on purpose. The author request needs post.author, so I wait for the post first. When you render the result, remember that the DOM work still matters. The guide on avoiding excessive DOM size is a good next step if you are building dynamic interfaces.
Common Mistakes
These are the async/await issues I look for first in real code:
- Forgetting
await. You get a Promise instead of the value you expected. - Using
awaitinsideforEach. The loop does not wait. - Writing empty
catchblocks. The failure disappears until users report it. - Making independent requests sequential. That performance mistake now has its own focused post with live demos.
- Adding
asyncwhen nothing awaits. Sometimes returning the original Promise is clearer.
FAQs
Common questions about using async and await in JavaScript.
async function always returns a Promise. A returned value becomes a resolved Promise, and a thrown error becomes a rejected Promise.await pauses the current async function and gives control back to the event loop. It does not freeze the browser.try/catch when you can handle the failure at that point. If the caller should decide what to do, let the rejection bubble up.forEach will not wait for the async callback. Use for...of for sequential work or Promise.all() with map() for parallel work.Promise.all() when several async operations are independent and you need all of them before continuing.Summary
async/await is not a different engine under the hood. It is a clearer way to write Promise-based code.
Use async when a function should return a Promise. Use await when you need to pause that function until a Promise settles. Use try/catch where you can actually handle failures. And when performance matters, make sure you are not waiting in sequence for work that could have started together.

