{}const=>[]async()letfn</>var
DevelopmentBasicsJS

Async/Await: A Complete Guide to Asynchronous Code

Let's figure out how to work with asynchronous code using async/await. Learn how to avoid callback hell, handle errors correctly, optimize parallel operations, and apply advanced patterns.

К

Kodik

Author

6 min read

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.

🔥 100,000+ students already with us

Tired of reading theory?
Time to code!

Kodik — an app where you learn to code through practice. AI mentor, interactive lessons, real projects.

🤖 AI 24/7
🎓 Certificates
💰 Free
🚀 Start learning
Joined today

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:

  • await can only be used inside async functions

  • async function always returns a promise

  • If 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 parallelism

Advanced 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!

🎯Stop procrastinating

Liked the article?
Time to practice!

In Kodik, you don't just read — you write code immediately. Theory + practice = real skills.

Instant practice
🧠AI explains code
🏆Certificate

No registration • No card