RxJS
Observable
Async
JavaScript
Programming

RxJS Observable - Wait for async method to complete before using next emitted value

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

If an observable emits values quickly but each value must go through asynchronous work before the next one should start, the operator choice matters more than the async function itself. In RxJS, the usual answer is concatMap, because it queues source emissions and waits for each inner async operation to complete before moving to the next one.

Why This Problem Happens

A source observable can emit values immediately, while asynchronous work such as HTTP requests, IndexedDB calls, or custom promises finishes later. If you flatten that work with the wrong operator, the results can overlap or arrive out of order.

typescript
import { of } from 'rxjs';

of(1, 2, 3).subscribe(value => console.log('source', value));

The source itself is ordered. The trouble starts when you turn each value into asynchronous work and flatten the result stream incorrectly.

Use concatMap for Sequential Async Work

concatMap subscribes to one inner observable at a time. It does not move on to the next source value until the current inner observable completes.

typescript
1import { of, from } from 'rxjs';
2import { concatMap } from 'rxjs/operators';
3
4async function saveItem(id: number): Promise<string> {
5  await new Promise(resolve => setTimeout(resolve, 100));
6  return `saved ${id}`;
7}
8
9of(1, 2, 3)
10  .pipe(concatMap(id => from(saveItem(id))))
11  .subscribe({
12    next: value => console.log(value),
13    error: error => console.error(error),
14  });

This guarantees that item 2 starts only after item 1 has completed, and item 3 waits for item 2.

Compare the Flattening Operators

This topic makes more sense when you contrast concatMap with the other common flattening operators:

  • 'concatMap: sequential, ordered, no overlap'
  • 'mergeMap: concurrent, results may complete in any order'
  • 'switchMap: cancels the previous inner operation when a new source value arrives'
  • 'exhaustMap: ignores new source values while one inner operation is active'

If your requirement says "wait for the current async task to finish before handling the next emitted value," that is almost a direct description of concatMap.

Common Angular or HTTP Example

A typical case is saving user actions in strict order.

typescript
1saveRequests$
2  .pipe(concatMap(payload => this.http.post('/api/save', payload)))
3  .subscribe({
4    next: () => console.log('saved in order'),
5    error: err => console.error(err),
6  });

Using mergeMap here could allow later requests to finish before earlier ones, which might be wrong for audit logs, ordered writes, or workflow state transitions.

Handling Errors Without Destroying the Entire Queue

By default, one inner error ends the whole outer stream. If you want to continue processing later values, catch the error inside the concatMap.

typescript
1import { of, from } from 'rxjs';
2import { catchError, concatMap } from 'rxjs/operators';
3
4of(1, 2, 3)
5  .pipe(
6    concatMap(id =>
7      from(saveItem(id)).pipe(
8        catchError(error => of(`failed ${id}: ${String(error)}`))
9      )
10    )
11  )
12  .subscribe(value => console.log(value));

This preserves sequential behavior while turning failures into data that the stream can continue past.

Watch Queue Growth

Sequential processing is correct for ordering, but it can build an internal backlog if the source emits faster than the async work completes. If that mismatch is large, you may also need to slow the source or batch work.

Useful companion operators include:

  • 'debounceTime when only the latest quiet-period value matters'
  • 'throttleTime when you want to limit burst rate'
  • buffering operators when you want to group items

Operator choice should reflect both correctness and throughput.

Avoid Mixing Promises and Observables Carelessly

RxJS can work with promises, but the boundary should stay explicit. Wrapping the promise in from(...) inside concatMap makes the intent clear and keeps the pipeline observable-based.

You can also return an observable directly if the async API already provides one. The important part is that the inner unit must complete before concatMap advances to the next source emission.

Common Pitfalls

The most common mistake is using mergeMap and then being surprised by overlapping or out-of-order completion. Another is using switchMap when every source value must be processed, which causes older work to be cancelled. Developers also forget that one inner error normally terminates the entire pipeline, so a single failure can stop the queue unless it is handled inside the mapped observable.

Summary

  • Use concatMap when each async operation must finish before the next one starts.
  • Choose flattening operators by required ordering and cancellation behavior, not by habit.
  • Handle inner errors explicitly if queue processing should continue.
  • Be aware that strict sequencing can build a backlog when the source emits too quickly.
  • Keep promise-to-observable conversion explicit with from(...) when needed.

Course illustration
Course illustration

All Rights Reserved.