Create your own
Lesson illustration

Parallelize Observables with `forkJoin`

Hello! Welcome back to our course on RxJS.

In our last lesson, we explored how to combine streams using merge, concat, zip, and combineLatest. We saw how these operators allow us to either flatten multiple streams into one or pair their values to create new, derived streams. This gave us a powerful toolkit for coordinating asynchronous events.

Today, we will focus on a specific, highly practical combination operator that addresses a very common need: running multiple asynchronous tasks in parallel and waiting for all of them to finish. This lesson covers the learning outcome: Execute and collect a group of Observables in parallel using forkJoin.

You'll find that forkJoin is the RxJS equivalent of Promise.all, a pattern you are likely very familiar with from your front-end development experience. We will explore its core behavior, critical "gotchas" regarding stream completion and error handling, and a powerful real-world pattern for fetching related data.

1. The Core Concept: Promise.all for Observables

At its heart, forkJoin is straightforward. It takes a collection of source observables, subscribes to all of them at once, and waits. Once all of the source observables have completed, forkJoin emits a single value. This value is an array (or an object) containing the last emitted value from each of the source streams.

This animated diagram provides a perfect visual intuition for this process.

This diagram shows two source streams. The `forkJoin` operator waits for both to complete (indicated by the vertical bar), then emits a single array containing the last value from each ('5' and 'D').

To get started, let's read a short introduction that directly compares forkJoin to Promise.all.

Combining Observables with forkJoin in RxJS

The article 'Combining Observables with forkJoin in RxJS' from Ultimate Courses provides a clear introduction to the operator and its relationship with Promise.all.

Read the beginning of the article, from the top down to just before the 'Order and parallel execution' section. This will solidify the basic concept and the Promise.all analogy.

2. Key Characteristics of forkJoin

While the basic idea is simple, there are a few key behaviors to understand to use forkJoin effectively.

Parallel Execution and Order Preservation

forkJoin subscribes to all source observables concurrently, meaning they execute in parallel. However, the output array is not dependent on the completion order. The results are mapped to their original positions. The first observable in the input array corresponds to the first value in the output array, regardless of when it completes.

Output Format: Array vs. Object

Since RxJS v6.5, you can pass forkJoin an object (or dictionary) of observables instead of an array. This can make your code much more readable, as you can destructure the results by name instead of accessing them by index.

Let's read about these two characteristics.

Combining Observables with forkJoin in RxJS

Let's continue with the same Ultimate Courses article to explore these behaviors.

Read the sections 'Order and parallel execution' and 'Alternative output options with forkJoin'. The code examples clearly demonstrate both points.

Here's a quick comparison of the two output formats:

Array Input (classic):

forkJoin([
  this.http.get('/user'),
  this.http.get('/settings')
]).subscribe(([user, settings]) => {
  // ...
});

Object Input (recommended for readability):

forkJoin({
  user: this.http.get('/user'),
  settings: this.http.get('/settings')
}).subscribe(({ user, settings }) => {
  // ...
});

3. The Critical Gotcha: Stream Completion

This is the single most important rule to remember about forkJoin: if any source observable does not complete, forkJoin will never emit a value.

This is a major difference from combineLatest, which emits as soon as it has one value from each source. forkJoin waits for the complete notification from every single source.

This is why forkJoin is perfect for finite streams like HTTP requests (which emit one value and then complete), but will cause silent failures if used with infinite streams like fromEvent (e.g., button clicks) or a raw interval unless you explicitly force them to complete.

Combining Observables with forkJoin in RxJS

The Ultimate Courses article has an excellent section that demonstrates this exact problem and its solution.

Read the section 'Inner observable lifetime and forkJoin'. Pay close attention to the example using interval and how the take operator is used to solve the problem by making the stream finite.

4. Error Handling Strategies

What happens if one of the parallel requests fails? By default, if any source observable errors, the entire forkJoin immediately errors out, and you lose the results from any other observables that might have completed successfully.

There are two primary strategies for handling this:

  1. Outer catchError: Place a catchError on the forkJoin stream itself. This is an "all or nothing" approach. If anything fails, you handle the error, but you get no results.
  2. Inner catchError: Place a catchError on each individual source observable that might fail. This is the more robust approach. It allows you to handle a failure within one stream (e.g., by returning a default value like null or a specific error object) while still allowing the forkJoin to complete successfully with the results from the other streams.

Let's see how this works in practice.

Combining Observables with forkJoin in RxJS

We'll read one more section from the Ultimate Courses article to understand these two error handling patterns.

Read the section 'Error handling with forkJoin'. Compare the output of the 'outer catch' example with the 'inner catch' example to see the difference in behavior.

For most UI applications where you want to display as much data as possible, even if one API call fails, the inner catchError strategy is almost always the correct choice.

5. Advanced Pattern: Fetching Related Data

Now let's look at a common, real-world scenario that perfectly demonstrates the power of forkJoin. Imagine you fetch a primary resource (like a product), and that resource contains an array of IDs pointing to related resources (like a list of supplier IDs). To display the full product details, you must then make a separate API call for each supplier.

This is a "one-to-many" data fetching pattern. forkJoin is the ideal tool for this. You can map the array of IDs into an array of observables (API calls) and then pass that array to forkJoin.

The following video demonstrates this pattern beautifully. The presenter first implements it using a more complex chain of operators (mergeMap and toArray) and then refactors it to a clean, single forkJoin, which is a powerful illustration of its utility.

RxJS in Angular: Terms, Tips, and Patterns

In this clip from 'RxJS in Angular: Terms, Tips, and Patterns', Deborah Kurata demonstrates exactly how to handle fetching multiple related data items.

Watch the segment from 36:13 to 40:22. Focus on the problem setup (a product with an array of supplierIds) and the final, elegant solution using forkJoin.

This pattern of mapping an array of items to an array of observables and then using forkJoin is extremely common and powerful. It declaratively expresses "for this list of things, perform an async action for each, and notify me when they are all done."

6. Quick Summary

To wrap up our exploration, let's watch a very quick summary that recaps the main idea of forkJoin.

RxJS Quick Start with Practical Examples

This short clip from Fireship's 'RxJS Quick Start with Practical Examples' provides a concise summary of forkJoin's behavior.

Watch the clip from 11:30 to 12:09. It's a quick and effective reinforcement of what we've learned.

Conclusion

In this lesson, we've taken a deep dive into forkJoin, the RxJS solution for parallel execution. You've seen that while it's conceptually similar to Promise.all, its observable nature requires careful attention to stream completion and error handling.

Key Takeaways:

  • Parallel Execution: forkJoin is used to run a group of observables in parallel.
  • Waits for Completion: It only emits a value after every single source observable has completed. If one never completes, forkJoin never emits.
  • Last Value Only: The final emission contains only the last value from each source.
  • Error Handling is Key: An unhandled error in any source will cause the entire forkJoin to fail. Use catchError on inner observables to build more resilient data-fetching logic.
  • Primary Use Case: Ideal for making multiple, independent API requests and combining their results to prepare the initial state for a component or view.

In our next lesson, we will shift our focus to a different, but equally powerful, set of operators: the flattening operators (mergeMap, switchMap, concatMap, and exhaustMap). While forkJoin is for running a known set of streams in parallel, flattening operators are used to handle scenarios where one stream triggers another, such as a user input event triggering a new API request. This will be a critical building block for creating interactive, real-world applications.

Can't find a good explanation? Sign up and we'll make it for you

Sign up