Why This Matters
Async code is everywhere:
- API calls
- File uploads
- Animations
- Data fetching
If you do not fully understand async behavior, you will face:
- Race conditions
- UI glitches
- Hard-to-debug bugs
The Problem with Callbacks
Before promises:
getUser(function (user) {
getPosts(user.id, function (posts) {
console.log(posts)
})
})
Problems:
- Nested code (callback hell)
- Error handling is messy
- Hard to maintain
Enter Promises
A Promise represents a future value.
States:
- Pending
- Fulfilled
- Rejected
Basic Example
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('Done'), 1000)
})
Consuming Promises
promise.then(result => console.log(result)).catch(error => console.error(error))
Chaining
fetchUser()
.then(user => fetchPosts(user.id))
.then(posts => console.log(posts))
Problem with Promises
Even with chaining:
- Still not very readable
- Harder debugging
Async/Await (Game Changer)
Syntax:
async function loadData() {
const user = await fetchUser()
const posts = await fetchPosts(user.id)
return posts
}
Why it is better:
- Looks synchronous
- Easier to read
- Cleaner logic
Under the Hood
async/await is syntactic sugar over promises.
It still:
- Uses event loop
- Returns a promise
Error Handling
With promises:
fetchUser().then(...).catch(...)
With async/await:
try {
const user = await fetchUser()
} catch (err) {
console.error(err)
}
Parallel vs Sequential Execution
Sequential (slow):
const a = await fetchA()
const b = await fetchB()
Parallel (fast):
const [a, b] = await Promise.all([fetchA(), fetchB()])
Event Loop (Critical Concept)
JavaScript is single-threaded but async works via:
- Call stack
- Web APIs
- Callback queue
- Microtask queue (Promises)
Promises run in microtask queue, which has higher priority.
Common Mistakes
1. Forgetting await
const data = fetchData() // returns Promise, not resolved data
2. Blocking Execution
await fetchA()
await fetchB() // unnecessary delay when independent
3. Mixing styles badly
Avoid:
await fetchUser().then(...)
Real-World Patterns
1. API Layer
export async function getUser() {
const res = await fetch('/api/user')
return res.json()
}
2. React Example
useEffect(() => {
async function load() {
const data = await getUser()
setUser(data)
}
load()
}, [])
3. Retry Logic
async function retry(fn, retries = 3) {
try {
return await fn()
} catch (e) {
if (retries === 0) throw e
return retry(fn, retries - 1)
}
}
When Not to Use Async/Await
- Complex streams: use Observables (RxJS)
- Real-time data flows
- Event-based systems
Conclusion
Promises and async/await are foundational.
Mastering them means:
- Better performance
- Cleaner code
- Fewer bugs


