Understanding Async/Await in JavaScript: A Practical Guide
If you’ve ever had to fetch data from an API, read a file, or wait for a timer, you’ve run into asynchronous code. And if you’ve tried to handle that with callbacks or promises, you know how quickly it can get messy. Async/await isn’t magic — but it does make async code look and behave more like the synchronous code we’re used to. Let’s cut through the noise and talk about how to actually use it.
What Is Async/Await?
async/await is syntactic sugar over JavaScript’s existing promise system. It doesn’t change how promises work — it just makes them easier to read and write.
An async function always returns a promise. Inside it, await pauses execution until the promise settles (resolves or rejects). That’s it.
async function getData() {
const response = await fetch('/api/data');
const data = await response.json();
return data;
}
This looks clean. No .then(), no nested callbacks. But under the hood, it’s still promise-based.
💡 Pro tip:
awaitonly works insideasyncfunctions. Try using it elsewhere, and you’ll get a syntax error.
Error Handling: Try/Catch, Not .catch()
With .then().catch(), error handling is chained. With async/await, use try/catch.
async function fetchUserData() {
try {
const response = await fetch('/api/user');
if (!response.ok) throw new Error('Network error');
const user = await response.json();
return user;
} catch (error) {
console.error('Fetch failed:', error);
// Handle or rethrow
}
}
This is more familiar if you’re used to synchronous error handling. But don’t forget: if you don’t catch, the error becomes an unhandled rejection.
⚠️ Always handle errors. Even if it’s just logging them.
You Can’t “Partially” Await
One common gotcha: await is all or nothing per statement.
// This waits for each sequentially
const user = await fetch('/api/user').then(r => r.json());
const posts = await fetch('/api/posts').then(r => r.json());
That’s fine if one depends on the other. But if they’re independent, you’re adding unnecessary latency.
To run them in parallel, use Promise.all():
async function loadUserAndPosts() {
try {
const [user, posts] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json())
]);
return { user, posts };
} catch (error) {
console.error('One of the requests failed', error);
}
}
Now both requests fire at the same time. Much faster.
✅ Rule of thumb: if promises don’t depend on each other, run them in parallel.
Don’t Forget: Async Functions Return Promises
This trips people up:
async function getUsername() {
const user = await fetch('/api/user').then(r => r.json());
return user.name;
}
// This doesn't give you the name directly
const name = getUsername(); // ❌ This is a promise!
You still have to await or .then() the result:
getUsername().then(name => console.log(name));
// or
const name = await getUsername(); // inside another async function
🧠 Remember:
asyncfunctions don’t return values — they return promises that resolve to values.
Avoiding Common Anti-Patterns
1. await on Every Line (When You Don’t Need To)
// ❌ Slower than it needs to be
const response = await fetch('/api/data');
const data = await response.json();
return data;
You can’t parallelize response.json() because it depends on fetch. But this is still fine — just don’t assume every await is a performance hit.
2. Wrapping Everything in async
// ❌ Not needed
async function logMessage(msg) {
console.log(msg);
}
// ✅ Just a normal function
function logMessage(msg) {
console.log(msg);
}
async has overhead. Only use it when you need await or are returning a promise.
Real-World Example: Retrying a Failed Request
Here’s a practical use case: retrying a flaky API call.
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(response.status);
return await response.json();
} catch (error) {
if (i === retries - 1) throw error; // Last try failed
console.log(`Retry ${i + 1} after error:`, error);
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); // Exponential backoff
}
}
}
// Usage
fetchWithRetry('/api/data')
.then(data => console.log('Success:', data))
.catch(err => console.error('Failed after retries:', err));
This is hard to write cleanly with .then(). With async/await, it’s readable and maintainable.
When Not to Use Async/Await
Sometimes
☕ Community-Focused
Top comments (0)