Asynchronous programming is one of the most important concepts in JavaScript, especially when working with APIs, databases, file operations, and network requests.
In this blog, I will explain three important concepts:
- Async/Await
- Promise Combinators
- Concurrency Limiting
As part of my Week 05 Task 02, I also implemented custom versions of Promise.all(), Promise.race(), and Promise.allSettled(), along with a concurrency-limited task runner.
1. What is Asynchronous JavaScript?
JavaScript normally executes code from top to bottom:
console.log("Start");
console.log("Middle");
console.log("End");
Output:
Start
Middle
End
But some operations take time, such as:
- API requests
- Database queries
- Reading files
- Timers
- Network requests
JavaScript uses Promises to represent the future result of these operations.
const promise = fetch("https://example.com");
The request does not immediately give us the final response. Instead, it gives us a Promise that will eventually be fulfilled or rejected.
2. Promises
A Promise can be in one of three states:
Pending
↓
Fulfilled
or:
Pending
↓
Rejected
For example:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data received");
}, 1000);
});
Initially the Promise is pending.
After one second, it becomes fulfilled with:
Data received
We can consume it using .then():
promise.then((value) => {
console.log(value);
});
3. Async/Await
async and await provide a cleaner way to work with Promises.
Instead of:
fetchData()
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error(error);
});
We can write:
async function getData() {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error(error);
}
}
The important thing to understand is that await does not block the entire JavaScript program.
It pauses the execution of the current async function until the Promise settles, while JavaScript can continue handling other work.
4. Sequential vs Parallel Async Operations
Consider three API requests:
const user = await getUser();
const posts = await getPosts();
const likes = await getLikes();
These operations run sequentially.
If each takes one second, the total time can be approximately:
1 second + 1 second + 1 second = 3 seconds
If the operations are independent, we can run them concurrently:
const [user, posts, likes] = await Promise.all([
getUser(),
getPosts(),
getLikes(),
]);
The total time can be closer to:
max(1s, 1s, 1s) = 1 second
This is one of the main reasons Promise combinators are useful.
5. Promise Combinators
JavaScript provides several useful Promise combinators.
The three implemented in this task are:
Promise.all()Promise.race()Promise.allSettled()
6. Promise.all()
Promise.all() waits for all Promises to fulfill.
const result = await Promise.all([
Promise.resolve("A"),
Promise.resolve("B"),
Promise.resolve("C"),
]);
console.log(result);
Output:
["A", "B", "C"]
An important property of Promise.all() is that it preserves the input order.
Even if the second operation finishes first, the result remains in the same order as the input.
Fail-fast behavior
If one Promise rejects:
await Promise.all([
Promise.resolve("A"),
Promise.reject("Something failed"),
Promise.resolve("C"),
]);
The returned Promise rejects.
This is called fail-fast behavior.
The other operations that have already started are not automatically cancelled.
7. My Custom Promise.all()
For this task, I implemented:
customAll(iterable)
The implementation:
- Converts the iterable into an array.
- Handles an empty iterable.
- Processes every item.
- Stores results using the original index.
- Resolves only after everything fulfills.
- Rejects when an item rejects.
The index is important because it allows the implementation to preserve input order.
Conceptually:
results[index] = value;
So completion order and result order can be different.
8. Promise.race()
Promise.race() settles when the first Promise settles.
const result = await Promise.race([
delay(1000, "Slow"),
delay(100, "Fast"),
]);
console.log(result);
Output:
Fast
For example:
Task A → 1000 ms
Task B → 200 ms
Task C → 500 ms
Winner → Task B
The first fulfilled or rejected Promise determines the result.
9. My Custom Promise.race()
I implemented:
customRace(iterable)
Each input is converted into a Promise-like value.
Then each one tries to settle the same resulting Promise.
Conceptually:
items.forEach((item) => {
Promise.resolve(item).then(
resolve,
reject
);
});
Only the first settlement matters.
An empty iterable remains pending, matching the behavior of native Promise.race().
10. Promise.allSettled()
Sometimes we don't want the entire operation to fail just because one Promise failed.
That is where Promise.allSettled() is useful.
const result = await Promise.allSettled([
Promise.resolve("Success"),
Promise.reject("Failed"),
Promise.resolve("Another success"),
]);
The result looks like:
[
{
status: "fulfilled",
value: "Success"
},
{
status: "rejected",
reason: "Failed"
},
{
status: "fulfilled",
value: "Another success"
}
]
Unlike Promise.all(), allSettled() waits for every operation.
11. When Should We Use Each Combinator?
| Combinator | Behavior |
|---|---|
Promise.all() |
Wait for everything, fail if one rejects |
Promise.race() |
Take the first settled result |
Promise.allSettled() |
Wait for everything and report every result |
Promise.all()
Useful when every operation is required.
Example:
Load user + permissions + profile
Promise.race()
Useful when the first result matters.
Example:
Try multiple servers and use the first response.
Promise.allSettled()
Useful when every result should be inspected.
Example:
Send notifications to 100 users and inspect which succeeded or failed.
12. Concurrency
Now we come to another important concept: concurrency.
Imagine we have 100 tasks:
const tasks = [
task1,
task2,
task3,
// ...
task100
];
If we start all 100 at once, we could have:
100 tasks running
↓
High resource usage
↓
Too many network requests
↓
Possible API rate limits
↓
System overload
Sometimes we need to control how many tasks can run at the same time.
That is called concurrency limiting.
13. What Does a Concurrency Limit Mean?
Suppose:
limit = 3
This means:
At most 3 tasks are allowed to run at the same time.
The basic flow is:
Start 3 tasks
↓
One finishes
↓
Start the next task
↓
One finishes
↓
Start the next task
↓
Repeat until everything completes
The important rule is:
active tasks <= concurrency limit
14. Why Tasks Must Be Functions
One important design decision is that the task list contains functions, not already-created Promises.
Correct:
const tasks = [
() => fetchUser(1),
() => fetchUser(2),
() => fetchUser(3),
];
Why?
Because a function lets the concurrency limiter decide when the operation starts.
If we instead write:
const tasks = [
fetchUser(1),
fetchUser(2),
fetchUser(3),
];
the requests may already have started before the limiter gets control.
So the function acts like a delayed instruction:
Task function
↓
Wait in queue
↓
Limiter starts it
↓
Promise begins
15. My Concurrency Limiter
I implemented:
runWithConcurrency(tasks, limit)
For example:
const results = await runWithConcurrency(
[
() => delay(300, "A"),
() => delay(100, "B"),
() => delay(200, "C"),
() => delay(150, "D"),
],
2
);
The limit is 2, so only two tasks can run simultaneously.
The basic algorithm is:
Start up to LIMIT tasks
↓
One task finishes
↓
Start the next queued task
↓
Repeat
↓
All tasks complete
16. Preserving Result Order
Suppose tasks finish in this order:
Task 2
Task 3
Task 1
Task 4
The returned result should still be:
[
result1,
result2,
result3,
result4
]
To achieve this, the implementation stores every result using the task's original index:
results[index] = value;
So completion order and result order can be different.
17. Handling Errors
The concurrency limiter rejects if a task fails.
For example:
const tasks = [
() => successfulTask(),
() => failingTask(),
() => anotherTask(),
];
If one task rejects, the returned Promise rejects.
However, tasks that were already running cannot generally be cancelled automatically.
This is an important distinction:
Promise rejection
≠
Automatic cancellation
JavaScript Promises do not provide generic cancellation by themselves.
When supported by the underlying API, AbortController can be used for cancellation.
18. Input Validation
The implementation also validates the arguments.
For example, the concurrency limit must be a positive integer.
Invalid:
runWithConcurrency(tasks, 0);
runWithConcurrency(tasks, -1);
runWithConcurrency(tasks, 1.5);
Valid:
runWithConcurrency(tasks, 3);
The implementation also checks that every task is a function.
19. Testing the Implementation
The tests cover the main behaviors.
Promise.all
Tested:
- Multiple fulfilled values
- Input ordering
- Rejection
- Empty iterable
- Native Promise compatibility
Promise.race
Tested:
- Fastest settlement
- Rejection
- Empty iterable behavior
Promise.allSettled
Tested:
- Fulfilled results
- Rejected results
- Mixed results
- Input ordering
- Empty iterable
Concurrency limiter
Tested:
- Maximum concurrency
- Result ordering
- Task failures
- Invalid limits
- Invalid task values
- Empty task arrays
Run the tests with:
node week-05/Task-02/test.js
Expected output:
All Week 05 Task 02 tests passed.
20. Concurrency vs Parallelism
These terms are related but not exactly the same.
Concurrency means multiple tasks can make progress during the same period.
Parallelism means tasks are literally executing at the same time, usually using multiple CPU cores or workers.
For JavaScript asynchronous I/O, we commonly talk about concurrency because multiple operations can be in progress while the JavaScript thread continues handling other work.
21. Real-World Example
Imagine an application needs to download 1,000 images.
A naive implementation might start everything immediately:
await Promise.all(
images.map((image) => download(image))
);
This can create a very large number of active operations.
A concurrency limiter can instead do:
await runWithConcurrency(
images.map((image) => () => download(image)),
5
);
Now only five downloads are active at a time.
This gives us better control over resource usage and external service pressure.
22. What I Learned
Through this task, I learned that asynchronous programming is not only about writing async and `await.
The important concepts are:
- How Promises work
- How Promise states change
- How
Promise.all()coordinates multiple operations - How
Promise.race()selects the first settlement - How
Promise.allSettled()collects every outcome - Why input ordering matters
- Why concurrency needs to be controlled
- Why queued tasks should be functions
- Why Promise rejection does not automatically cancel running operations
- How to test asynchronous behavior reliably
The biggest takeaway for me was understanding the difference between:
text
Starting everything immediately
and:
text
Controlling when asynchronous work starts
That distinction becomes very important when building real applications.
23. Conclusion
Async/Await makes asynchronous JavaScript easier to read, but Promise combinators provide the tools needed to coordinate multiple asynchronous operations.
`text
Promise.all()
↓
Everything must succeed
Promise.race()
↓
First settlement matters
Promise.allSettled()
↓
Every result matters
`
Concurrency limiting adds another layer of control:
text
Large number of tasks
↓
Task queue
↓
Concurrency limit
↓
Controlled execution
↓
Ordered results
Understanding these concepts gives a strong foundation for working with APIs, databases, file operations, background jobs, and other asynchronous systems in JavaScript.
Repository
The implementation for this task is part of my Week 05 learning project.
Branch:
text
week-05-task-02-async-await-combinators
Task directory:
text
week-05/Task-02/
Files:
text
promise-combinators.js
concurrency-limiter.js
test.js
README.md
Final Takeaway
When working with asynchronous JavaScript, ask three questions:
-
Do I need all operations to succeed?
- Use
Promise.all().
- Use
-
Do I only care about the first result?
- Use
Promise.race().
- Use
-
Do I need to know the result of every operation?
- Use
Promise.allSettled().
- Use
And when there are many tasks:
Don't just start everything. Control how much work runs at once.
That is the core idea behind concurrency limiting.
Top comments (0)