JavaScript Async/Await
JavaScript Async/Await Interview with follow-up questions
1. What is async/await in JavaScript and why is it used?
Async/await is syntactic sugar over Promises that lets you write asynchronous code with a synchronous-looking control flow. You mark a function async, then use await to pause that function until a Promise settles — without blocking the main thread, since the event loop keeps running other work in the meantime.
async function getUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
Why it's used: it flattens .then() chains, makes error handling natural with try/catch, and keeps async logic readable as code grows.
Key points interviewers probe:
- An
asyncfunction always returns a Promise; areturnvalue becomes the resolved value, athrowbecomes a rejection. awaitonly suspends the enclosing async function, not the whole program.- Top-level
awaitworks in ES modules, so you canawaitat module scope without an IIFE wrapper. - Common gotcha: awaiting in a loop runs requests sequentially. For independent work, kick them off together and
await Promise.all([...]).
Follow-up 1
Can you explain how async/await works with promises?
Async/await works with promises by allowing you to write asynchronous code in a more synchronous style. When you use the await keyword before a promise, it pauses the execution of the function until the promise is resolved or rejected.
Here's an example:
async function getData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
}
getData()
.then(data => console.log(data))
.catch(error => console.error(error));
In this example, the fetch function returns a promise that resolves to a response object. By using await, we can pause the execution of the getData function until the promise is resolved. We can then use await again to pause the execution until the response is converted to JSON. The final result is returned as a promise, which can be handled with then and catch.
Follow-up 2
What are the advantages of using async/await over traditional callback functions?
There are several advantages of using async/await over traditional callback functions:
Readability: Async/await allows you to write asynchronous code that looks and behaves like synchronous code. This makes it easier to read and understand, especially for developers who are new to asynchronous programming.
Error handling: Async/await simplifies error handling by allowing you to use try/catch blocks. This makes it easier to handle and propagate errors, compared to the callback-style error handling.
Sequencing: Async/await allows you to write asynchronous code in a sequential manner, making it easier to reason about the order of operations. This is especially useful when you have multiple asynchronous operations that depend on each other.
Debugging: Async/await makes it easier to debug asynchronous code, as the execution flow is more predictable and easier to follow compared to callback functions.
Follow-up 3
Can you provide a simple example of async/await in use?
Sure! Here's a simple example that demonstrates the use of async/await:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
fetchData();
In this example, the fetchData function is declared as async, which means it returns a promise. Inside the function, we use await to pause the execution until the fetch promise is resolved. We then use await again to pause the execution until the response is converted to JSON. Finally, we log the data to the console. If any error occurs during the execution, it is caught and logged to the console.
Follow-up 4
What are some common pitfalls when using async/await?
When using async/await, there are a few common pitfalls to be aware of:
Forgetting to use the
awaitkeyword: It's important to remember to use theawaitkeyword before promises, otherwise the code will not pause and the function will continue executing.Not handling errors properly: Async/await simplifies error handling with try/catch blocks, but it's important to handle errors properly. If an error is not caught, it will be propagated as an unhandled promise rejection.
Mixing async/await with callback-style code: Async/await is designed to work with promises, so mixing it with callback-style code can lead to confusion and errors. It's best to stick to one style of asynchronous programming.
Not returning a promise: When declaring an async function, it automatically returns a promise. It's important to ensure that the function returns a promise, otherwise the behavior may not be as expected.
Follow-up 5
How does error handling work with async/await?
Error handling with async/await is done using try/catch blocks. When an error occurs inside an async function, it is thrown as an exception. The try block is used to wrap the code that may throw an error, and the catch block is used to handle the error.
Here's an example:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
fetchData();
In this example, if an error occurs during the execution of the fetch or response.json statements, it will be caught by the catch block. The error object can then be accessed and handled within the catch block. If no error occurs, the code inside the try block will be executed without any interruption.
2. How does async/await help in writing asynchronous code in JavaScript?
Async/await is syntactic sugar over Promises that lets asynchronous code read top-to-bottom like synchronous code, while still being non-blocking. Instead of nesting .then() callbacks, you await each Promise and assign its result to a variable.
// Promise chain
function load() {
return fetch('/api/data')
.then(r => r.json())
.then(data => process(data));
}
// async/await equivalent
async function load() {
const r = await fetch('/api/data');
const data = await r.json();
return process(data);
}
What it actually improves:
- Linear control flow — branches, loops, and
try/catchwork the way they do in synchronous code, so error handling no longer needs a trailing.catch(). - Cleaner debugging — stack traces and breakpoints map to real lines rather than anonymous callbacks.
- It doesn't make code faster or change concurrency — under the hood it's still Promises and the microtask queue. For parallel work you still reach for
Promise.all;await-ing one call at a time stays sequential.
Follow-up 1
What is the difference between async/await and promises?
The main difference between async/await and promises is the syntax and the way they handle asynchronous code. Promises use the then/catch methods to handle asynchronous operations, while async/await uses the keywords async and await to write asynchronous code in a more synchronous-like manner. Async/await is built on top of promises and provides a more intuitive and readable way to work with asynchronous operations.
Follow-up 2
Can async/await be used with any function?
Async/await can be used with any function that returns a promise. It allows you to write asynchronous code in a synchronous style, making it easier to work with promises. However, if a function does not return a promise, you cannot use async/await with it directly. In such cases, you can wrap the function call in a new promise or use other techniques to handle the asynchronous behavior.
Follow-up 3
How can we handle exceptions in async/await?
Exceptions in async/await can be handled using try/catch blocks. When an exception is thrown inside an async function, it is caught by the nearest try/catch block. You can use the catch block to handle the exception and perform any necessary error handling or fallback logic. If an exception is not caught, it will be propagated up the call stack and can be caught by an outer try/catch block or result in an unhandled promise rejection.
Follow-up 4
Can you nest async functions?
Yes, you can nest async functions. Async functions can contain other async functions, and you can use the await keyword to wait for the completion of the inner async function before continuing with the outer async function. This allows you to write more complex asynchronous code by breaking it down into smaller, more manageable functions.
Follow-up 5
What happens when an async function is called?
When an async function is called, it returns a promise. The promise will be resolved with the value returned by the async function, or rejected with the exception thrown by the async function. The code inside the async function is executed asynchronously, and the execution will pause at any point where the await keyword is used until the awaited promise is resolved or rejected. This allows other code to continue running while the async function is waiting for the completion of the asynchronous operation.
3. What is the difference between async/await and generators in JavaScript?
Both let a function pause and resume, but they exist for different purposes and async/await is actually built on the same suspension machinery generators use.
Generators (
function*+yield) are general-purpose: they produce a sequence of values lazily and you drive them manually by calling.next(). They're about iteration and cooperative pausing, not asynchrony by themselves.Async functions (
async+await) are purpose-built for Promises. They auto-drive: eachawaitsuspends the function and the runtime resumes it when the awaited Promise settles. An async function always returns a Promise.
function* counter() {
yield 1;
yield 2; // resumes here on next .next()
}
async function load() {
const data = await fetch('/api/x').then(r => r.json());
return data; // resumes after the Promise settles
}
The connection interviewers like to hear: before async/await existed, people emulated it with a generator that yields Promises plus a runner library (e.g. co). Async/await is essentially that pattern standardized into the language. So generators are the lower-level primitive; async/await is the specialized, self-running form for asynchronous code.
Follow-up 1
When would you use generators over async/await?
Generators can be useful in scenarios where you need fine-grained control over the flow of execution. For example, if you need to implement a custom iterator or if you want to build a complex state machine. Generators allow you to pause and resume the execution of a function, which can be helpful in these situations.
On the other hand, async/await is generally easier to use and understand, especially for simple asynchronous operations. It provides a more straightforward syntax for handling promises and is generally recommended for most asynchronous programming tasks.
Follow-up 2
Can you explain with an example?
Sure! Here's an example that demonstrates the difference between async/await and generators:
// Async/await example
async function getData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
}
// Generators example
function* getData() {
const response = yield fetch('https://api.example.com/data');
const data = yield response.json();
return data;
}
Follow-up 3
Can generators and async/await be used together?
Yes, generators and async/await can be used together. You can use a generator function as an iterator in an async function, and you can also use the yield* syntax to delegate to another generator function.
Here's an example that demonstrates the use of generators and async/await together:
async function* getData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
yield data;
}
async function processData() {
const iterator = getData();
const result = await iterator.next();
console.log(result.value);
}
Follow-up 4
What are the performance implications of using async/await vs generators?
In terms of performance, async/await and generators have different characteristics:
Async/await is generally more efficient and has better performance compared to generators. This is because async/await is built on top of promises, which are optimized for performance.
Generators, on the other hand, have some performance overhead due to the additional machinery required to pause and resume the execution of a function.
However, it's important to note that the performance difference between async/await and generators is usually negligible in most real-world scenarios. The choice between them should be based on the specific requirements of your application and the level of control you need over the flow of execution.
Follow-up 5
What are the compatibility issues with async/await and generators?
Async/await and generators are both supported in modern versions of JavaScript, but there are some compatibility issues to be aware of:
Async/await is supported in most modern browsers and Node.js versions. However, it may not be supported in older browsers or outdated versions of Node.js. To ensure compatibility, you can use a transpiler like Babel to convert async/await code to a version of JavaScript that is supported by your target environment.
Generators are also supported in modern browsers and Node.js versions, but they may require a transpiler to work in older environments. Additionally, there are some differences in the implementation of generators between different JavaScript engines, so you may encounter compatibility issues when using advanced generator features.
It's always a good idea to check the compatibility of async/await and generators with your target environment before using them in production code.
4. How can async/await be used with fetch API in JavaScript?
fetch returns a Promise, so it pairs naturally with await. Define an async function, await the response, then await the body parser (.json(), .text(), etc.). Wrap it in try/catch for error handling.
async function fetchData() {
try {
const res = await fetch('https://api.example.com/data');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error('Fetch failed:', err);
throw err; // let the caller decide
}
}
Gotchas interviewers expect you to know:
fetchonly rejects on network failure, not on 4xx/5xx. You must checkres.ok(orres.status) yourself.- The body is a one-shot stream — you can read it once. Don't call
.json()twice on the same response. - Cancellation / timeouts use an
AbortController:
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 5000);
const res = await fetch(url, { signal: ctrl.signal });
clearTimeout(t);
fetch is native in browsers and in Node 18+, so the same code runs on both. For parallel requests, fire them off and await Promise.all([...]) rather than awaiting each in turn.
Follow-up 1
Can you provide a code example?
Sure! Here is a code example that demonstrates how to use async/await with the fetch API in JavaScript:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
fetchData();
In this example, the fetchData function is defined as an async function. Inside the function, the await keyword is used to wait for the response from the fetch request. Once the response is received, the data is extracted using the json() method. If there is an error during the fetch request or data extraction, it is caught and logged to the console.
Follow-up 2
How does error handling work in this case?
In the case of using async/await with the fetch API in JavaScript, error handling can be done using try-catch blocks. When an error occurs during the fetch request or data extraction, it will be caught by the catch block. Here is an example:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
fetchData();
In this example, if there is an error during the fetch request or data extraction, it will be caught by the catch block and logged to the console using console.error().
Follow-up 3
What happens when multiple async functions are used with fetch API?
When multiple async functions are used with the fetch API in JavaScript, each function will be executed independently. This means that each function can make its own fetch requests and handle the responses separately. Here is an example:
async function fetchData1() {
const response = await fetch('https://api.example.com/data1');
const data = await response.json();
console.log(data);
}
async function fetchData2() {
const response = await fetch('https://api.example.com/data2');
const data = await response.json();
console.log(data);
}
fetchData1();
fetchData2();
In this example, fetchData1 and fetchData2 are two separate async functions. Each function makes its own fetch request and handles the response independently.
Follow-up 4
How can we handle timeouts with async/await and fetch API?
To handle timeouts with async/await and the fetch API in JavaScript, you can use the Promise.race() method along with a timeout promise. Here is an example:
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Request timed out'));
}, ms);
});
}
async function fetchData() {
try {
const response = await Promise.race([
fetch('https://api.example.com/data'),
timeout(5000)
]);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
fetchData();
In this example, the timeout function returns a promise that rejects with an error after a specified number of milliseconds. The Promise.race() method is then used to race between the fetch request and the timeout promise. If the fetch request takes longer than the specified timeout, the timeout promise will reject and the error will be caught by the catch block.
Follow-up 5
What are the alternatives to using async/await with fetch API?
There are several alternatives to using async/await with the fetch API in JavaScript. Some of the alternatives include:
Using promises: Instead of using async/await, you can use promises to handle asynchronous operations with the fetch API. Promises provide a more explicit and granular way of handling asynchronous code.
Using callback functions: Another alternative is to use callback functions to handle asynchronous operations with the fetch API. Callback functions can be passed as arguments to the fetch API methods and executed when the asynchronous operation is complete.
Using third-party libraries: There are also third-party libraries available, such as Axios and Superagent, that provide additional features and flexibility for making HTTP requests in JavaScript.
The choice of alternative depends on the specific requirements and preferences of your project.
5. What are the best practices to follow when using async/await in JavaScript?
Best practices interviewers look for:
Handle errors with
try/catch. A rejected awaited Promise throws, so wrap awaits intry/catch. Don't leave async functions unhandled — an unawaited rejection becomes anunhandledRejection.Parallelize independent work with
Promise.all. The classic anti-pattern is awaiting inside a loop when the iterations don't depend on each other:
// Slow: sequential
for (const id of ids) results.push(await getUser(id));
// Fast: concurrent
const results = await Promise.all(ids.map(getUser));
Use Promise.allSettled when you want every result even if some fail.
Don't wrap a Promise in
new Promise. If a function already returns a Promise, justreturnorawaitit — thenew Promise(...)constructor here is an anti-pattern and swallows errors.Always
await(orreturn) the Promise. A forgottenawaitmakestry/catchmiss the error and lets code run before the work finishes.Use top-level
awaitin modules instead of an IIFE wrapper when you need async setup at module scope.Add timeouts/cancellation for network calls via
AbortController, so a hung request can't stall the flow.Don't mix
awaitwith.then()/callbacks for the same flow — pick one style for readability.
Follow-up 1
Can you explain why each of these practices is important?
Using try-catch blocks to handle errors is important because it allows you to catch and handle any errors that may occur during the execution of async/await code. Without proper error handling, unhandled errors can crash your application.
Using async functions for better code organization is important because it helps separate the async/await logic from the main code, making it easier to read, understand, and maintain. It also allows for better error handling and code reusability.
Using Promise.all() to run multiple async operations concurrently is important for improving performance. By running async operations concurrently, you can reduce the overall execution time of your code.
Avoiding mixing async/await with callbacks is important for code clarity and maintainability. Mixing these two styles of asynchronous programming can lead to confusing and hard-to-follow code.
Proper error handling for rejected promises is important to prevent unhandled promise rejections. When a promise is rejected, it will throw an error. By handling these errors properly, you can prevent your application from crashing and provide appropriate error messages to the user.
Using async/await with caution in loops is important to prevent slower execution times. If each iteration of a loop depends on the previous one to complete, it can lead to slower execution. By using techniques like Promise.all() or running async operations in parallel, you can improve the performance of your code.
Follow-up 2
How can we avoid 'callback hell' with async/await?
Async/await is a powerful feature in JavaScript that helps avoid 'callback hell' by allowing you to write asynchronous code in a more synchronous and readable manner. Here are a few ways to avoid 'callback hell' with async/await:
Use async functions: Encapsulate your async/await logic in separate async functions. This helps break down complex asynchronous operations into smaller, more manageable functions.
Use try-catch blocks: Wrap your await statements in try-catch blocks to handle errors. This helps prevent unhandled errors and makes your code more robust.
Use Promise.all(): If you have multiple async operations that can run independently, you can use Promise.all() to run them concurrently and wait for all of them to complete. This helps avoid nested callbacks and improves code readability.
Use helper functions: If you find yourself repeating the same async/await pattern multiple times, consider creating helper functions to encapsulate that logic. This helps reduce code duplication and makes your code more modular.
By following these practices, you can write cleaner and more maintainable asynchronous code with async/await.
Follow-up 3
What are some common mistakes developers make when using async/await?
Here are some common mistakes developers make when using async/await:
Forgetting to use the 'await' keyword: When calling an async function, it's important to use the 'await' keyword to wait for the function to complete. Forgetting to use 'await' will result in the function returning a promise instead of the expected result.
Not handling errors properly: Async/await code can still throw errors, and it's important to handle them properly using try-catch blocks or by chaining a .catch() method to the promise. Failing to handle errors can lead to unhandled promise rejections and crashes.
Mixing async/await with callbacks: Mixing async/await with callbacks can lead to confusing and hard-to-maintain code. It's best to stick to one style of asynchronous programming to keep the codebase consistent and easier to understand.
Not using Promise.all() when appropriate: If you have multiple async operations that can run independently, not using Promise.all() to run them concurrently can result in slower execution times. Using Promise.all() can significantly improve performance.
By being aware of these common mistakes, you can write more reliable and efficient async/await code.
Follow-up 4
How can we test async functions?
Testing async functions in JavaScript can be done using various testing frameworks and libraries. Here's a general approach to testing async functions:
Use a testing framework: Choose a testing framework like Jest, Mocha, or Jasmine that supports testing async functions.
Write test cases: Create test cases for your async functions, covering different scenarios and edge cases.
Use async/await or Promises: Depending on the testing framework, you can use async/await or Promises to handle async code. With async/await, you can use the 'await' keyword to wait for the async function to complete. With Promises, you can use the .then() and .catch() methods to handle the resolved or rejected promises.
Use assertions: Use assertions provided by the testing framework to verify the expected behavior of your async functions. Assertions can be used to check if the returned values are correct, if errors are thrown when expected, and if promises are resolved or rejected as expected.
By following these steps, you can effectively test your async functions and ensure their correctness and reliability.
Follow-up 5
What tools can help in debugging async/await code?
Debugging async/await code can be challenging, but there are several tools that can help:
Chrome DevTools: The Chrome browser's DevTools provide powerful debugging capabilities for JavaScript, including async/await code. You can set breakpoints, step through code, inspect variables, and view call stacks to debug async/await code.
Node.js Inspector: If you're working with Node.js, you can use the Node.js Inspector to debug async/await code. It provides similar debugging capabilities as Chrome DevTools.
Visual Studio Code: Visual Studio Code is a popular code editor that provides built-in debugging support for JavaScript. It allows you to debug async/await code using breakpoints, stepping through code, and inspecting variables.
Async hooks: Node.js provides async hooks that allow you to track the lifecycle of async operations. You can use async hooks to debug and trace async/await code.
By using these tools, you can effectively debug async/await code and identify and fix any issues or bugs.
6. Can you explain the event loop, and the difference between microtasks and macrotasks?
One of the most-asked JavaScript questions, because it explains how async works. JavaScript runs on a single thread with an event loop: synchronous code runs to completion on the call stack, and asynchronous callbacks wait in queues until the stack is empty.
There are two queues with different priority:
- Microtask queue — Promise callbacks (
.then/catch/finally),awaitcontinuations,queueMicrotask,MutationObserver. - Macrotask (task) queue —
setTimeout/setInterval, I/O, UI events,setImmediate(Node).
The rule: after each macrotask (and after the initial script), the engine drains the ENTIRE microtask queue before rendering or running the next macrotask. This is why Promises always resolve before a setTimeout(…, 0):
console.log('A');
setTimeout(() => console.log('B')); // macrotask
Promise.resolve().then(() => console.log('C')); // microtask
console.log('D');
// Output: A, D, C, B
Follow-ups: a flood of microtasks (or an infinite Promise loop) can starve rendering and macrotasks; await yields control back to the loop (it doesn't block the thread); CPU-bound work still blocks everything — offload to a Web Worker.
Live mock interview
Mock interview: JavaScript Async/Await
- Read your scene and goals
- Talk it out; goals tick off live
- Get a score and stronger lines
Your voice and your AI key never touch our servers; the key stays in this browser and is sent only to Google. Only your round scores are saved to track progress.