Why do we need asynchrony?
Imagine that your application makes a request to an external API that responds in 2 seconds. Without asynchrony, the program will simply freeze for this time, blocking the execution of other operations. Asynchronous code allows the application to continue working while the operation is running in the background.
From callbacks to promises and async/await
The evolution of asynchronous code in JavaScript has gone through several stages:
Callback functions (outdated approach):
fetchUser(userId, (error, user) => {
if (error) {
console.error(error);
return;
}
fetchPosts(user.id, (error, posts) => {
if (error) {
console.error(error);
return;
}
// Callback hell starts here
});
});Promises (improvement):
fetchUser(userId)
.then(user => fetchPosts(user.id))
.then(posts => console.log(posts))
.catch(error => console.error(error));Async/Await (modern approach):
async function getUserPosts(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
return posts;
} catch (error) {
console.error(error);
}
}Basics of async/await
The async keyword before the function means that the function always returns a promise. The await keyword causes JavaScript to wait for the result of the promise before continuing execution.
async function fetchData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
}Important rules:
awaitcan only be used insideasyncfunctionsasyncfunction always returns a promiseIf the function returns a value, it is automatically wrapped in
Promise.resolve()
Error handling
Try-catch blocks are a natural way to handle errors in async/await:
async function processOrder(orderId) {
try {
const order = await fetchOrder(orderId);
const payment = await processPayment(order);
const confirmation = await sendConfirmation(payment);
return confirmation;
} catch (error) {
if (error.code === 'PAYMENT_FAILED') {
await refundOrder(orderId);
}
throw new Error(`Order processing failed: ${error.message}`);
}
}Can be combined with .catch() for more granular processing:
async function getData() {
const data = await fetchData().catch(error => {
console.error('Fetch failed:', error);
return getDefaultData(); // Returning default data
});
return data;
}
Parallel execution
One of the common mistakes is the sequential execution of independent operations:
// Bad: operations are performed sequentially (6 seconds)
async function loadData() {
const users = await fetchUsers(); // 3 seconds
const posts = await fetchPosts(); // 3 seconds
return { users, posts };
}Use Promise.all() for parallel execution:
// Good: operations are performed in parallel (3 seconds)
async function loadData() {
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
]);
return { users, posts };
}Promise.allSettled()
For cases where the results of all operations are needed:
async function loadAllData() {
const results = await Promise.allSettled([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Operation ${index} succeeded:`, result.value);
} else {
console.error(`Operation ${index} failed:`, result.reason);
}
});
}Promise.race()
For operations with timeout:
async function fetchWithTimeout(url, timeout = 5000) {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), timeout)
);
return Promise.race([
fetch(url),
timeoutPromise
]);
}Async/await in loops
Be careful with loops — they can create unexpected behavior:
// Sequential processing
async function processSequentially(items) {
for (const item of items) {
await processItem(item); // Waiting for each operation to complete
}
}
// Parallel processing
async function processInParallel(items) {
await Promise.all(items.map(item => processItem(item)));
}
// Batch processing
async function processBatches(items, batchSize = 5) {
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
await Promise.all(batch.map(item => processItem(item)));
}
}Common mistakes
1. Forgotten await
// Error: the function will return a promise, not data
async function getData() {
return fetchData(); // Forgot await
}
// Correct
async function getData() {
return await fetchData();
}2. Using await in forEach
// Does not work: forEach does not understand async
items.forEach(async (item) => {
await processItem(item);
});
// Use for...of
for (const item of items) {
await processItem(item);
}3. Creating promisses in the cycle without control
// Creates thousands of simultaneous requests
const promises = items.map(item => fetchItem(item));
await Promise.all(promises);
// Better control of parallelismAdvanced patterns
Retry with exponential delay:
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fetch(url);
} catch (error) {
if (i === maxRetries - 1) throw error;
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}Caching results:
const cache = new Map();
async function fetchWithCache(key, fetcher) {
if (cache.has(key)) {
return cache.get(key);
}
const result = await fetcher();
cache.set(key, result);
return result;
}Debounce for async functions:
function asyncDebounce(func, wait) {
let timeout;
return function(...args) {
return new Promise((resolve) => {
clearTimeout(timeout);
timeout = setTimeout(async () => {
resolve(await func.apply(this, args));
}, wait);
});
};
}Async/await in other languages
The async/await concept exists not only in JavaScript:
Python:
async def fetch_data():
response = await aiohttp.get('https://api.example.com')
return await response.json()C#:
async Task<string> FetchDataAsync() {
var response = await httpClient.GetAsync("https://api.example.com");
return await response.Content.ReadAsStringAsync();
}Rust:
async fn fetch_data() -> Result<String, Error> {
let response = reqwest::get("https://api.example.com").await?;
Ok(response.text().await?)
}Performance and best practices
1. Avoid excessive await
// Suboptimal
async function process() {
const result = await computeValue();
return result; // Extra await
}
// Better
async function process() {
return computeValue();
}2. Use parallelism where possible
// Slow
const user = await getUser();
const posts = await getPosts();
// Faster
const [user, posts] = await Promise.all([getUser(), getPosts()]);3. Add timeouts for external requests
async function safeFetch(url) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url, { signal: controller.signal });
return response;
} finally {
clearTimeout(timeout);
}
}Conclusion
Async/await makes asynchronous code readable and understandable, eliminating callback hell problems and simplifying error handling. Key points:
Use async/await for sequential operations
Use Promise.all() for parallel execution
Don't forget to handle errors with try-catch
Be careful with loops and always use await where needed
Control parallelism to avoid system overload
Practice and understanding of when operations should be performed sequentially and when in parallel will help you write effective asynchronous code.
Code offers structured courses in JavaScript, Python, C, Rust, and many other languages, where you can learn asynchronous programming from the basics to advanced patterns. Interactive lessons with practical tasks will help to consolidate the material on real examples.
Do you have questions about async/await or are you facing a difficult task? Join our Telegram channel, where experienced developers and an active community are always ready to help you deal with any problem — from syntactic subtleties to architectural solutions. Ask questions, share experiences, and learn with the community!
