Reading Time 5
Number of Words 1013
JavaScript operates mainly on a single thread, which means it can execute only one piece of code at a time.
To handle tasks like I/O, network requests, timers, or events without blocking the main flow, it uses asynchrony.
Understanding how synchrony and asynchrony work, and how to work with callbacks, promises, and async/await—is essential in writing responsive, nonblocking code.
Table of Contents
- Synchronous vs Asynchronous: What’s the Difference?
- Callback Functions
- Problems with Callbacks: “Callback Hell”
- Promises: Clean Asynchronous Control Flow
- Chaining Promises
- Error Handling in Promises
async/awaitSyntax- Real-World Examples
- Best Practices & Tips
1. Synchronous vs Asynchronous: What’s the Difference?
- Synchronous (blocking): Code is executed line by line. Each operation must complete before the next starts.
- Asynchronous (non-blocking): Some operations can start and continue in the background while later code runs; when done, they “notify” via callbacks, promises, or events.
Example:
console.log('Start');
setTimeout(() => {
console.log('Asynchronous callback');
}, 1000);
console.log('End');
Output:
Start
End
Asynchronous callback
Even though setTimeout is called before console.log('End'), its callback happens later—this is asynchrony at work.
2. Callback Functions
A callback is a function passed as an argument to another function, to be executed later (when some operation completes).
function fetchData(callback) {
setTimeout(() => {
const data = { name: 'Alice', age: 25 };
callback(data);
}, 1000);
}
console.log('Fetching...');
fetchData((result) => {
console.log('Data received:', result);
});
console.log('This may run before data arrives');
While callbacks are simple and straightforward, they have drawbacks:
- Nesting many operations leads to deeply nested code (callback hell)
- Hard to handle errors and coordinate multiple asynchronous tasks
3. Problems with Callbacks: “Callback Hell”
When you have multiple sequential asynchronous operations, callbacks can become nested like this:
doStep1((res1) => {
doStep2(res1, (res2) => {
doStep3(res2, (res3) => {
doStep4(res3, (res4) => {
console.log('Final result', res4);
});
});
});
});
This is hard to read, maintain, and handle errors properly. That’s where promises come in.
4. Promises: Clean Asynchronous Control Flow
A Promise is an object representing the eventual result (or failure) of an asynchronous operation. It has three states:
- pending (initial state)
- fulfilled (operation succeeded)
- rejected (operation failed)
You create a promise using the Promise constructor:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = { name: 'Alice', age: 25 };
// Suppose success condition:
if (data) {
resolve(data);
} else {
reject(new Error('No data found'));
}
}, 1000);
});
}
// Usage
fetchData()
.then((result) => {
console.log('Data:', result);
})
.catch((error) => {
console.error('Error:', error);
});
With promises, the code becomes flatter and easier to reason about.
5. Chaining Promises
Promises support chaining, so you can sequence asynchronous operations without deep nesting:
fetchData()
.then((data) => {
console.log('First fetch:', data);
return fetchDataFromAnotherSource(data);
})
.then((moreData) => {
console.log('Second fetch:', moreData);
return doAnotherAsync(moreData);
})
.then((final) => {
console.log('Final:', final);
})
.catch((error) => {
console.error('Error in chain:', error);
});
Each .then returns a new promise, so you can keep chaining.
6. Error Handling in Promises
Error handling is more consistent with promises:
- Use
.catch(...)at the end of a chain to handle any rejection in the chain - You can also catch in the middle:
fetchData()
.then((data) => {
return doSomethingAsync(data);
})
.catch((err) => {
console.error('Error during doSomething:', err);
// Optionally rethrow or recover
})
.then((next) => {
return anotherAsync(next);
})
.catch((err) => {
console.error('Error later:', err);
});
If a .then callback throws an error or returns a rejected promise, it’s caught in the next .catch.
7. async / await Syntax
async / await is syntactic sugar over promises that lets you write asynchronous code as though it were synchronous, improving readability.
async function main() {
try {
const data = await fetchData();
console.log('Received:', data);
const more = await fetchMore(data);
console.log('More:', more);
const final = await anotherAsync(more);
console.log('Final:', final);
} catch (error) {
console.error('Error in async function:', error);
}
}
main();
Inside an async function:
awaitpauses execution until the promise resolves or rejects- If rejected, an exception is thrown and can be caught with
try/catch
You can return values (they are wrapped in a promise) or throw errors inside async functions.
8. Real-World Examples
Example 1: Fetching Data from an API
async function getUserData(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error('Network response was not OK');
}
const data = await response.json();
return data;
}
getUserData(123)
.then((user) => {
console.log('User data:', user);
})
.catch((err) => {
console.error('Failed to fetch user:', err);
});
Example 2: Run Multiple Promises in Parallel
async function loadAll() {
const [posts, comments, profile] = await Promise.all([
fetch('/posts').then(res => res.json()),
fetch('/comments').then(res => res.json()),
fetch('/profile').then(res => res.json())
]);
console.log({ posts, comments, profile });
}
loadAll().catch(err => console.error(err));
Here, Promise.all waits for all the promises to resolve (or rejects if any fails).
Example 3: Sequential and Conditional Async Operations
async function process(id) {
const user = await getUserData(id);
if (!user.isActive) {
throw new Error('User is inactive');
}
const content = await fetchContent(user.pref);
return content;
}
process(42)
.then((content) => console.log('Content:', content))
.catch((err) => console.error('Error:', err));
9. Best Practices & Tips
- Prefer promises or async/await over raw callbacks for better readability and maintainability
- Always handle errors—either
.catchortry/catchin async functions - Use
Promise.all,Promise.race,Promise.allSettledorPromise.anyto manage multiple concurrent promises - If you have asynchronous loops, use
for … ofwithawait, notArray.forEach - Be cautious with long chains; break into helper functions when needed
- Consider cancellation (AbortController) for fetch or long-running operations
- Don’t mix callback-style and promise style within the same function — be consistent