DEV Community

Sai Swaroop Bijinapalli
Sai Swaroop Bijinapalli

Posted on

Asynchronous JavaScript II: Promise Combinators & Concurrency Control

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
Enter fullscreen mode Exit fullscreen mode

1. First: What is asynchronous JavaScript?

JavaScript normally executes code sequentially.

console.log("Start");
console.log("Middle");
console.log("End");
Enter fullscreen mode Exit fullscreen mode

Output:

Start
Middle
End
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

Output:

Start
End
Data received
Enter fullscreen mode Exit fullscreen mode

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);

});
Enter fullscreen mode Exit fullscreen mode

The Promise starts as:

pending
Enter fullscreen mode Exit fullscreen mode

After 2 seconds:

pending
   ↓
fulfilled
Enter fullscreen mode Exit fullscreen mode

with:

value = "Data received"
Enter fullscreen mode Exit fullscreen mode

3. async/await

async/await provides a cleaner way to work with Promises.

async

When we write:

async function getData() {

}
Enter fullscreen mode Exit fullscreen mode

the function automatically returns a Promise.

Example:

async function hello() {
    return "Hello";
}
Enter fullscreen mode Exit fullscreen mode

Although we return a string:

return "Hello";
Enter fullscreen mode Exit fullscreen mode

the function actually returns a Promise.

hello().then((value) => {
    console.log(value);
});
Enter fullscreen mode Exit fullscreen mode

Output:

Hello
Enter fullscreen mode Exit fullscreen mode

Remember

An async function 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);

    });
}
Enter fullscreen mode Exit fullscreen mode

Now:

async function main() {

    const data = await getData();

    console.log(data);
}

main();
Enter fullscreen mode Exit fullscreen mode

Output after 2 seconds:

Data received
Enter fullscreen mode Exit fullscreen mode

What happens?

main()
 ↓
getData()
 ↓
Promise pending
 ↓
await
 ↓
wait for Promise
 ↓
resolve("Data received")
 ↓
data = "Data received"
 ↓
console.log(data)
Enter fullscreen mode Exit fullscreen mode

Important

await doesn't create the value.

The value is provided by:

resolve("Data received");
Enter fullscreen mode Exit fullscreen mode

Then:

const data = await getData();
Enter fullscreen mode Exit fullscreen mode

receives that value.

So:

resolve() → provides value
await → receives value
data → stores value
Enter fullscreen mode Exit fullscreen mode

5. Why use async/await?

Without async/await:

getData()
    .then((data) => {
        console.log(data);
    });
Enter fullscreen mode Exit fullscreen mode

With async/await:

async function main() {

    const data = await getData();

    console.log(data);
}
Enter fullscreen mode Exit fullscreen mode

Both work with Promises.

async/await makes asynchronous code easier to read.


6. Error handling with try/catch

Promises can either:

fulfill
Enter fullscreen mode Exit fullscreen mode

or:

reject
Enter fullscreen mode Exit fullscreen mode

Example:

function getData() {

    return new Promise((resolve, reject) => {

        setTimeout(() => {
            reject("Server error");
        }, 2000);

    });
}
Enter fullscreen mode Exit fullscreen mode

Using async/await:

async function main() {

    try {

        const data = await getData();

        console.log(data);

    } catch (error) {

        console.log("Error:", error);
    }
}

main();
Enter fullscreen mode Exit fullscreen mode

Output:

Error: Server error
Enter fullscreen mode Exit fullscreen mode

Flow

await getData()
       ↓
Promise rejects
       ↓
error thrown
       ↓
catch(error)
       ↓
handle error
Enter fullscreen mode Exit fullscreen mode

Mentor answer

"try/catch allows us to handle rejected Promises when using async/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();
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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);

    });
Enter fullscreen mode Exit fullscreen mode

Output:

[
    "User",
    "Profile",
    "Posts"
]
Enter fullscreen mode Exit fullscreen mode

Important

The result order follows the input order.

Promise.all([
    p1,
    p2,
    p3
]);
Enter fullscreen mode Exit fullscreen mode

Result:

[p1 result, p2 result, p3 result]
Enter fullscreen mode Exit fullscreen mode

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);

    });
Enter fullscreen mode Exit fullscreen mode

Output:

Profile failed
Enter fullscreen mode Exit fullscreen mode

Promise.all() rejects when one Promise rejects.

This is called:

Fail-fast

Promise 1 → success
Promise 2 → failure
               ↓
          Promise.all()
               ↓
             reject
Enter fullscreen mode Exit fullscreen mode

When should you use it?

Use Promise.all() when all operations are required.

Example:

Load user
Load permissions
Load account settings
Enter fullscreen mode Exit fullscreen mode

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);

    });
Enter fullscreen mode Exit fullscreen mode

Result:

[
    {
        status: "fulfilled",
        value: "User"
    },
    {
        status: "rejected",
        reason: "Profile failed"
    },
    {
        status: "fulfilled",
        value: "Posts"
    }
]
Enter fullscreen mode Exit fullscreen mode

It doesn't stop because p2 failed.

This is called:

Fail-soft

Promise 1 → success ─┐
Promise 2 → failure ─┤
Promise 3 → success ─┤
                     ↓
              allSettled()
                     ↓
              collect everything
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);

    });
Enter fullscreen mode Exit fullscreen mode

Output:

First
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

then:

Promise.race()
      ↓
reject
Enter fullscreen mode Exit fullscreen mode

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);

    });
Enter fullscreen mode Exit fullscreen mode

Output:

Server 2
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Promise.any()

First to fulfill
     ↓
success only
Enter fullscreen mode Exit fullscreen mode

Example:

P1 → reject in 1 sec
P2 → fulfill in 2 sec
Enter fullscreen mode Exit fullscreen mode

race():

P1 rejects first
↓
race rejects
Enter fullscreen mode Exit fullscreen mode

any():

P1 rejects → ignored
↓
P2 fulfills
↓
any returns P2
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

15. Generators

Now another concept: Generators.

A generator is a special function that can pause and resume execution.

We declare one using:

function*
Enter fullscreen mode Exit fullscreen mode

Example:

function* numbers() {

    yield 1;
    yield 2;
    yield 3;

}
Enter fullscreen mode Exit fullscreen mode

Create the generator:

const generator = numbers();
Enter fullscreen mode Exit fullscreen mode

Then:

console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
Enter fullscreen mode Exit fullscreen mode

Output:

{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: true }
Enter fullscreen mode Exit fullscreen mode

16. What does yield do?

yield pauses the generator.

function* numbers() {

    yield 1;

    yield 2;

    yield 3;
}
Enter fullscreen mode Exit fullscreen mode

Flow:

next()
 ↓
yield 1
 ↓
pause

next()
 ↓
yield 2
 ↓
pause

next()
 ↓
yield 3
 ↓
pause

next()
 ↓
done
Enter fullscreen mode Exit fullscreen mode

So:

yield produces a value and pauses execution.


17. Iterators

An iterator is an object that provides a next() method.

next() returns:

{
    value: ...,
    done: ...
}
Enter fullscreen mode Exit fullscreen mode

For example:

const generator = numbers();

console.log(generator.next());
Enter fullscreen mode Exit fullscreen mode

gives:

{
    value: 1,
    done: false
}
Enter fullscreen mode Exit fullscreen mode

Generators automatically provide iterator behavior.


18. for...of

Instead of manually calling:

generator.next();
Enter fullscreen mode Exit fullscreen mode

we can use:

function* numbers() {

    yield 10;
    yield 20;
    yield 30;

}

for (const number of numbers()) {

    console.log(number);

}
Enter fullscreen mode Exit fullscreen mode

Output:

10
20
30
Enter fullscreen mode Exit fullscreen mode

19. Async Iterators

An async iterator is useful when values become available asynchronously.

We create an async generator using:

async function*
Enter fullscreen mode Exit fullscreen mode

Example:

async function* getData() {

    yield "User";

    await new Promise(resolve => {
        setTimeout(resolve, 1000);
    });

    yield "Profile";

    await new Promise(resolve => {
        setTimeout(resolve, 1000);
    });

    yield "Posts";
}
Enter fullscreen mode Exit fullscreen mode

We consume it using:

async function main() {

    for await (const data of getData()) {

        console.log(data);

    }
}

main();
Enter fullscreen mode Exit fullscreen mode

Output:

User
Profile
Posts
Enter fullscreen mode Exit fullscreen mode

The values are produced asynchronously.


20. for...of vs for await...of

Normal iterator

for (const value of iterable) {
    console.log(value);
}
Enter fullscreen mode Exit fullscreen mode

Used for synchronous values.

Async iterator

for await (const value of asyncIterable) {
    console.log(value);
}
Enter fullscreen mode Exit fullscreen mode

Used when values may arrive asynchronously.

Remember:

for...of
    ↓
synchronous iteration

for await...of
    ↓
asynchronous iteration
Enter fullscreen mode Exit fullscreen mode

21. Concurrency

Now connect everything to your implementation task.

Suppose you have 10 API calls:

API 1
API 2
API 3
...
API 10
Enter fullscreen mode Exit fullscreen mode

If you run all 10 at once, you have:

10 concurrent operations
Enter fullscreen mode Exit fullscreen mode

Sometimes you want a limit:

Maximum = 3
Enter fullscreen mode Exit fullscreen mode

So only:

API 1
API 2
API 3
Enter fullscreen mode Exit fullscreen mode

run initially.

When one finishes:

API 4
Enter fullscreen mode Exit fullscreen mode

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`);
}
Enter fullscreen mode Exit fullscreen mode

If we start everything directly:

runTask(1, 2000);
runTask(2, 1000);
runTask(3, 1500);
runTask(4, 1000);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

⭐ Most important things to remember

If your mentor asks you quickly:

What is async/await?

"async/await is syntax built on Promises that makes asynchronous code easier to read. async makes a function return a Promise, and await gets 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, while any() returns the first fulfilled Promise."

What is a generator?

"A generator is a function that can pause with yield and resume with next()."

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)