Understand polyfill for Async Await in JavaScript with a step-by-step explanation. This helps in understanding the internal functioning of Async Await in JavaScript.
Anuj Sharma
Last Updated Oct 4, 2025
Async Await in JavaScript provides a simple way to handle async operations synchronously. This removes the issues that come with callback hell while using promises to handle async operations in JavaScript. This is one of the most commonly asked questions to the senior devs to evaluate their in-depth understanding.
Understanding a polyfill for Async Await (the common approach is generator + Promise) is a great way to show deep knowledge of the event loop, Promise resolution, error propagation, and how transpilers like Babel
work. This post walks you through a clear polyfill, examples, test cases, and a step-by-step explanation.
Polyfill of Async Await will help to understand and reproduces the major runtime behaviours of Async Await for example returning a Promise, awaiting Promises (or non-Promise values), handling throw
/try-catch
, and preserving sequential control flow by pausing execution until an awaited Promise settles. This way the async await provides a sequential execution of the async code.
Let's first understand how async await works in the sequential and parallel calls. This will help to create the required test cases which async await polyfill needs to satisfy.
const delay = (ms, value, fail = false) =>
new Promise((resolve, reject) => {
setTimeout(() => (fail ? reject(value) : resolve(value)), ms);
});
// Example 1: Sequential awaits (runs one after the other)
async function sequentialFetch() {
const a = await delay(300, 1);
const b = await delay(300, 2);
return a + b; // ~600ms
}
// Example 2: Parallel with Promise.all
async function parallelFetch() {
const [a, b] = await Promise.all([delay(300, 1), delay(300, 2)]);
return a + b; // ~300ms
}
// Example 3: Error handling with await
async function fetchWithCatch() {
try {
const v = await delay(100, 'Error', true); // will reject
return v;
} catch (err) {
return 'caught: ' + err;
}
}
// Usage
sequentialFetch().then(console.log); // 3
parallelFetch().then(console.log); // 3
fetchWithCatch().then(console.log); // caught: Error
Its important to know what all to expect from async await polyfill, and make sure it satisfies these behaviours (or we can say test cases):
async
returns a Promise: Calling the wrapped function should immediately return a Promise.await 42
should give 42
(wrapped by Promise.resolve
).const a = await p1; const b = await p2;
should wait for p1
to settle before starting p2
's effective continuation.await Promise.all([p1,p2])
should run both p1
and p2
concurrently and wait for both.try/catch
).try
around await
should catch rejections from the awaited Promise.Below is a widely used pattern (conceptually equivalent to Babel's _asyncToGenerator
) that implements async
/ await
using generator functions + Promises. It's intentionally small so you can explain it in an interview.
/**
* Converts a generator function into an async-like function.
* @param {Function} genFn - The generator function.
* @returns {Function} A function that returns a Promise.
*/
export function asyncToGenerator(genFn) {
return function (...args) {
const self = this; // preserve `this` context
const gen = genFn.apply(self, args); // initialize generator
return new Promise((resolve, reject) => {
// Recursive step function
function step(nextFn, arg) {
let result;
try {
// Advance the generator
result = gen[nextFn](arg);
} catch (err) {
// If generator throws, reject outer promise
reject(err);
return;
}
const { value, done } = result;
if (done) {
// Generator completed
resolve(value);
} else {
// Await value (handle normal values too)
Promise.resolve(value).then(
val => step("next", val),
err => step("throw", err)
);
}
}
// Start execution
step("next");
});
};
}
/** This is what we generally call the API
async function getUserPosts() {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
return { ...user, posts };
}
*/
// Here is how we can call using the async Polyfill.
const fetchUser = () =>
new Promise(res => setTimeout(() => res({ id: 1, name: "Anuj Sharma" }), 500));
const fetchPosts = (userId) =>
new Promise(res =>
setTimeout(
() =>
res([
{ id: 101, userId, title: "FrontendGeek" },
{ id: 102, userId, title: "Building Polyfills" },
]),
500
)
);
// Using our polyfill
const getUserPosts = asyncToGenerator(function* () {
const user = yield fetchUser(); // wait for user
const posts = yield fetchPosts(user.id); // wait for posts
return { ...user, posts }; // final return value
});
// Execute like an async function
getUserPosts().then(console.log).catch(console.error);
// Output after 1 sec
{
id: 1,
name: "Anuj Sharma",
posts: [
{ id: 101, userId: 1, title: "FrontendGeek" },
{ id: 102, userId: 1, title: "Building Polyfills" }
]
}
You can also yield a Promise.all()
for parallel API calls, exactly like native async/await:
const getUserAndSettings = asyncToGenerator(function* () {
const [user, settings] = yield Promise.all([
fetchUser(),
new Promise(res => setTimeout(() => res({ theme: "dark" }), 500))
]);
return { ...user, settings };
});
getUserAndSettings().then(console.log);
Below is a detailed breakdown of every step inside our asyncToGenerator
helper and why it exists.
We start by creating a function (e.g., asyncPolyfill
) that takes a generator function as input. This generator will represent our “async” function that yields promises.
Inside the polyfill, we call the generator function to get a generator object. This gives us access to .next()
and .throw()
methods to control the flow.
step()
FunctionWe define a helper function step()
to move through the generator sequence. This function will:
next()
to get the next value.If the generator yields a promise, we wait for it to resolve. Once it resolves, we feed the resolved value back into the generator via next(value)
.
If it rejects, we handle the error using throw(error)
to keep the same behaviour as native async/await
.
We recursively call step()
until the generator signals it’s done (done: true
). At that point, we resolve the final promise with the generator’s return value.
Finally, our polyfill returns a Promise so that the entire async function behaves like a native one, allowing us to use .then()
or await
it externally.
This polyfill is intentionally small for clarity and interview explanation. Real transpilers (Babel + regenerator) add more features but the core idea is the same: generators
+ Promise-driven stepping produce
the async await polyfill in JavaScript.
Advertisement
Advertisement
Vijay Sai Krishna vsuri
Last Updated Aug 21, 2025
A Quick guide about popstate event in JavaScript, If you’ve ever hit the back button in your browser and wondered how your Single-Page Application knows which view to render, this guide is for you.
Alok Kumar Giri
Last Updated Jun 2, 2025
Code snippet examples which will help to grasp the concept of Hoisting in JavaScript, with solutions to understand how it works behind the scene.
Anuj Sharma
Last Updated Oct 2, 2025
Explore Polyfill for map, filter and reduce array methods in JavaScript. A detailed explanation of Map, filter and reduce polyfills in JS helps you to know the internal working of these array methods.
Anuj Sharma
Last Updated Aug 3, 2025
Explore the implementation of setTimeout in JavaScript with a detailed explanation for every step. Understand all scenarios expected to implement the setTimeout polyfill.
Frontendgeek
Last Updated Sep 25, 2025
Understand the JWT(JSON Web Token) and how JWT decode works. It also covers how the end-to-end JWT authentication works between client & server, along with the pros and cons of using JWT.
Anuj Sharma
Last Updated Sep 14, 2025
Learn hoisting in JavaScript with clear examples and explanations. Understand variable hoisting in JavaScript, function hoisting in JavaScript, and how the temporal dead zone affects hoisting in JS.
Subscribe to FrontendGeek Hub for the frontend interview preparation, interview experiences, curated resources and roadmaps.
© 2024 FrontendGeek. All rights reserved