Week-05 Task 02 — Asynchronous JavaScript II
The main topics are:
1. async / await
2. Error handling with try/catch
3. Promise.all()
4. Promise.allSettled()
5. Promise.race()
6. Promise.any()
7. Generators
8. Iterators
9. Async iterators
10. Fail-fast vs fail-soft
11. Concurrency limiting
1. First: What is asynchronous JavaScript?
JavaScript normally executes code sequentially.
console.log("Start");
console.log("Middle");
console.log("End");
Output:
Start
Middle
End
But some operations take time, such as:
- API requests
- Database requests
- File operations
- Timers
- Network requests
For example:
console.log("Start");
setTimeout(() => {
console.log("Data received");
}, 2000);
console.log("End");
Output:
Start
End
Data received
Why?
Because setTimeout() is asynchronous. JavaScript doesn't wait for the timer before continuing.
2. Why do we use Promises?
When an asynchronous operation finishes, we need a way to handle its result.
A Promise represents the future result of an asynchronous operation.
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data received");
}, 2000);
});
The Promise starts as:
pending
After 2 seconds:
pending
↓
fulfilled
with:
value = "Data received"
3. async/await
async/await provides a cleaner way to work with Promises.
async
When we write:
async function getData() {
}
the function automatically returns a Promise.
Example:
async function hello() {
return "Hello";
}
Although we return a string:
return "Hello";
the function actually returns a Promise.
hello().then((value) => {
console.log(value);
});
Output:
Hello
Remember
An
asyncfunction always returns a Promise.
4. await
await is used to get the result of a Promise.
function getData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Data received");
}, 2000);
});
}
Now:
async function main() {
const data = await getData();
console.log(data);
}
main();
Output after 2 seconds:
Data received
What happens?
main()
↓
getData()
↓
Promise pending
↓
await
↓
wait for Promise
↓
resolve("Data received")
↓
data = "Data received"
↓
console.log(data)
Important
await doesn't create the value.
The value is provided by:
resolve("Data received");
Then:
const data = await getData();
receives that value.
So:
resolve() → provides value
await → receives value
data → stores value
5. Why use async/await?
Without async/await:
getData()
.then((data) => {
console.log(data);
});
With async/await:
async function main() {
const data = await getData();
console.log(data);
}
Both work with Promises.
async/await makes asynchronous code easier to read.
6. Error handling with try/catch
Promises can either:
fulfill
or:
reject
Example:
function getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("Server error");
}, 2000);
});
}
Using async/await:
async function main() {
try {
const data = await getData();
console.log(data);
} catch (error) {
console.log("Error:", error);
}
}
main();
Output:
Error: Server error
Flow
await getData()
↓
Promise rejects
↓
error thrown
↓
catch(error)
↓
handle error
Mentor answer
"
try/catchallows us to handle rejected Promises when usingasync/await. If an awaited Promise rejects, the rejection is thrown as an error and the catch block handles it."
7. Promise Combinators
Suppose you have multiple asynchronous operations:
const user = getUser();
const profile = getProfile();
const posts = getPosts();
You may want to handle them together.
That's where Promise combinators are useful.
The four important combinators are:
Promise.all()
Promise.allSettled()
Promise.race()
Promise.any()
8. Promise.all()
Promise.all() waits for all Promises to fulfill.
Example:
const p1 = Promise.resolve("User");
const p2 = Promise.resolve("Profile");
const p3 = Promise.resolve("Posts");
Promise.all([p1, p2, p3])
.then((results) => {
console.log(results);
});
Output:
[
"User",
"Profile",
"Posts"
]
Important
The result order follows the input order.
Promise.all([
p1,
p2,
p3
]);
Result:
[p1 result, p2 result, p3 result]
even if p3 finishes first.
9. What happens when one fails?
const p1 = Promise.resolve("User");
const p2 = Promise.reject("Profile failed");
const p3 = Promise.resolve("Posts");
Promise.all([p1, p2, p3])
.then((results) => {
console.log(results);
})
.catch((error) => {
console.log(error);
});
Output:
Profile failed
Promise.all() rejects when one Promise rejects.
This is called:
Fail-fast
Promise 1 → success
Promise 2 → failure
↓
Promise.all()
↓
reject
When should you use it?
Use Promise.all() when all operations are required.
Example:
Load user
Load permissions
Load account settings
If one required operation fails, you don't want to continue.
10. Promise.allSettled()
Promise.allSettled() waits for every Promise to finish, whether it succeeds or fails.
Example:
const p1 = Promise.resolve("User");
const p2 = Promise.reject("Profile failed");
const p3 = Promise.resolve("Posts");
Promise.allSettled([p1, p2, p3])
.then((results) => {
console.log(results);
});
Result:
[
{
status: "fulfilled",
value: "User"
},
{
status: "rejected",
reason: "Profile failed"
},
{
status: "fulfilled",
value: "Posts"
}
]
It doesn't stop because p2 failed.
This is called:
Fail-soft
Promise 1 → success ─┐
Promise 2 → failure ─┤
Promise 3 → success ─┤
↓
allSettled()
↓
collect everything
When should you use it?
When each operation is independent.
For example:
Send email to User 1 → success
Send email to User 2 → failure
Send email to User 3 → success
You still want to know the result for every user.
11. Promise.race()
Promise.race() returns the result of the first Promise to settle.
Example:
const p1 = new Promise((resolve) => {
setTimeout(() => {
resolve("First");
}, 1000);
});
const p2 = new Promise((resolve) => {
setTimeout(() => {
resolve("Second");
}, 2000);
});
Promise.race([p1, p2])
.then((result) => {
console.log(result);
});
Output:
First
because p1 finished first.
Important
race() cares about settling, not just success.
If the first Promise rejects:
p1 → reject after 1 second
p2 → resolve after 2 seconds
then:
Promise.race()
↓
reject
So:
race()= first settled Promise wins.
12. Promise.any()
Promise.any() waits for the first fulfilled Promise.
Example:
const p1 = Promise.reject("Server 1 failed");
const p2 = new Promise((resolve) => {
setTimeout(() => {
resolve("Server 2");
}, 1000);
});
const p3 = new Promise((resolve) => {
setTimeout(() => {
resolve("Server 3");
}, 2000);
});
Promise.any([p1, p2, p3])
.then((result) => {
console.log(result);
});
Output:
Server 2
p1 failed, but Promise.any() ignores that rejection and waits for a successful Promise.
13. race() vs any()
This is important for interviews.
Promise.race()
First to settle
↓
success OR failure
Promise.any()
First to fulfill
↓
success only
Example:
P1 → reject in 1 sec
P2 → fulfill in 2 sec
race():
P1 rejects first
↓
race rejects
any():
P1 rejects → ignored
↓
P2 fulfills
↓
any returns P2
14. All four combinators together
Remember this:
| Method | Meaning |
|---|---|
Promise.all() |
All must succeed |
Promise.allSettled() |
Everyone must finish |
Promise.race() |
First to settle wins |
Promise.any() |
First success wins |
Easy memory trick:
ALL
↓
Everyone succeeds
ALLSETTLED
↓
Everyone finishes
RACE
↓
First result
ANY
↓
First success
15. Generators
Now another concept: Generators.
A generator is a special function that can pause and resume execution.
We declare one using:
function*
Example:
function* numbers() {
yield 1;
yield 2;
yield 3;
}
Create the generator:
const generator = numbers();
Then:
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
Output:
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: true }
16. What does yield do?
yield pauses the generator.
function* numbers() {
yield 1;
yield 2;
yield 3;
}
Flow:
next()
↓
yield 1
↓
pause
next()
↓
yield 2
↓
pause
next()
↓
yield 3
↓
pause
next()
↓
done
So:
yieldproduces a value and pauses execution.
17. Iterators
An iterator is an object that provides a next() method.
next() returns:
{
value: ...,
done: ...
}
For example:
const generator = numbers();
console.log(generator.next());
gives:
{
value: 1,
done: false
}
Generators automatically provide iterator behavior.
18. for...of
Instead of manually calling:
generator.next();
we can use:
function* numbers() {
yield 10;
yield 20;
yield 30;
}
for (const number of numbers()) {
console.log(number);
}
Output:
10
20
30
19. Async Iterators
An async iterator is useful when values become available asynchronously.
We create an async generator using:
async function*
Example:
async function* getData() {
yield "User";
await new Promise(resolve => {
setTimeout(resolve, 1000);
});
yield "Profile";
await new Promise(resolve => {
setTimeout(resolve, 1000);
});
yield "Posts";
}
We consume it using:
async function main() {
for await (const data of getData()) {
console.log(data);
}
}
main();
Output:
User
Profile
Posts
The values are produced asynchronously.
20. for...of vs for await...of
Normal iterator
for (const value of iterable) {
console.log(value);
}
Used for synchronous values.
Async iterator
for await (const value of asyncIterable) {
console.log(value);
}
Used when values may arrive asynchronously.
Remember:
for...of
↓
synchronous iteration
for await...of
↓
asynchronous iteration
21. Concurrency
Now connect everything to your implementation task.
Suppose you have 10 API calls:
API 1
API 2
API 3
...
API 10
If you run all 10 at once, you have:
10 concurrent operations
Sometimes you want a limit:
Maximum = 3
So only:
API 1
API 2
API 3
run initially.
When one finishes:
API 4
starts.
This is called concurrency limiting.
22. Simple concurrency example
async function runTask(id, time) {
console.log(`Task ${id} started`);
await new Promise(resolve => {
setTimeout(resolve, time);
});
console.log(`Task ${id} finished`);
}
If we start everything directly:
runTask(1, 2000);
runTask(2, 1000);
runTask(3, 1500);
runTask(4, 1000);
all four can run concurrently.
A concurrency limiter controls that.
Limit = 2
Task 1 ───────────
Task 2 ─────
Task 3 starts after Task 2 finishes
Task 4 starts when another slot is free
This is exactly what your concurrency-limiter.js task is designed to implement.
23. Complete Week-05 Task 02 picture
Asynchronous JavaScript II
│
├── async/await
│ ├── async
│ └── await
│
├── Error Handling
│ └── try/catch
│
├── Promise Combinators
│ ├── Promise.all()
│ ├── Promise.allSettled()
│ ├── Promise.race()
│ └── Promise.any()
│
├── Fail Strategies
│ ├── all() → fail-fast
│ └── allSettled() → fail-soft
│
├── Generators
│ ├── function*
│ └── yield
│
├── Iterators
│ └── next()
│
├── Async Iterators
│ ├── async function*
│ └── for await...of
│
└── Concurrency
└── limit simultaneous async tasks
⭐ Most important things to remember
If your mentor asks you quickly:
What is async/await?
"
async/awaitis syntax built on Promises that makes asynchronous code easier to read.asyncmakes a function return a Promise, andawaitgets the resolved value of a Promise."
What is Promise.all()?
"It waits for all Promises to fulfill and fails fast if one rejects."
What is Promise.allSettled()?
"It waits for all Promises to settle and gives the result of every Promise, whether fulfilled or rejected."
Difference between race() and any()?
"
race()returns the first settled Promise, whileany()returns the first fulfilled Promise."
What is a generator?
"A generator is a function that can pause with
yieldand resume withnext()."
What is an async iterator?
"An async iterator produces values asynchronously and can be consumed using
for await...of."
What is concurrency limiting?
"Concurrency limiting controls how many asynchronous operations can run at the same time, such as allowing only three API calls concurrently."
Top comments (0)