Skip to content

Understanding Timing and Sequencing ​

In RxJS "Why is the value not flowing?" Is the order wrong?" It is very common to face such problems. This page describes basic knowledge and practical debugging techniques to correctly understand timing and ordering.

When do values flow? ​

Problem: You think the value will flow immediately after subscribe ​

A misconception that many beginners fall into is that they will get the value immediately after subscribe.

❌ Bad example: expecting to get the value immediately ​

typescript
import { of } from 'rxjs';
import { delay } from 'rxjs';

let result: number | undefined;

of(42).pipe(
  delay(100)
).subscribe(value => {
  result = value;
});

console.log(result); // undefined (value not yet arrived)

✅ Good example: process within subscribe ​

typescript
import { of } from 'rxjs';
import { delay } from 'rxjs';

of(42).pipe(
  delay(100)
).subscribe(value => {
  console.log(value); // 42 is output after 100ms
});

Important principles

  • Observable can be asynchronous
  • Processes that use values should be done in subscribe
  • Do not expect values outside of subscribe

Understanding synchronous vs. asynchronous ​

Synchronous Observable vs. Asynchronous Observable ​

RxJS has both synchronous and asynchronous Observables.

Example of Synchronous Observable ​

typescript
import { of } from 'rxjs';

console.log('start');

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

console.log('End');

// Output:
// start
// value: 1
// value: 2
// value: 3
// End

Asynchronous Observable Example ​

typescript
import { interval } from 'rxjs';
import { take } from 'rxjs';

console.log('start');

interval(100).pipe(
  take(3)
).subscribe(value => {
  console.log('value:', value);
});

console.log('End');

// Output:
// start
// End
// value: 0  (after 100ms)
// value: 1  (after 200ms)
// value: 2  (after 300ms)

Visualize synchronous vs. asynchronous execution flow ​

The following sequence diagram illustrates the difference in execution timing between synchronous Observable and Observable.

Timing differences

  • Synchronous Observable: complete processing in subscribe before moving on to the next line
  • Asynchronous Observable: subscribe returns immediately, values flow later

Criteria for synchronous/asynchronous ​

ObservableSynchronous/AsynchronousReason
of(1, 2, 3)SynchronousValue is determined immediately
from([1, 2, 3])SynchronousImmediately available from array
interval(1000)AsynchronousTimer takes time
fromEvent(button, 'click')AsynchronousWaits for user interaction
ajax('/api/data')AsynchronousWait for HTTP request
timer(1000)AsynchronousFire after 1 second
of(1).pipe(delay(100))AsynchronousDelayed by delay

Common problem: mixing synchronous and asynchronous ​

❌ Bad example: no ordering guaranteed ​

typescript
import { of } from 'rxjs';
import { delay } from 'rxjs';

console.log('1: start');

of('sync').subscribe(value => {
  console.log('2:', value);
});

of('asynchronous').pipe(
  delay(0) // asynchronous even at 0ms
).subscribe(value => {
  console.log('3:', value);
});

console.log('4: end');

// Output:
// 1: start
// 2: sync
// 4: end
// 3: asynchronous  ← delay(0) also queues asynchronously

✅ Good example: clarify intent ​

typescript
import { of, concat } from 'rxjs';
import { delay } from 'rxjs';

// Use concat if you want to guarantee ordering
concat(
  of('first'),
  of('next').pipe(delay(100)),
  of('last')
).subscribe(value => {
  console.log(value);
});

// Output:
// first
// next    (after 100ms)
// last    (after 100ms)

How to read a Marble Diagram ​

The Marble Diagram is a diagram that visualizes the behavior of Observable on a time axis.

Basic Notation ​

Time axis:  ------a----b----c----|
            ↑     ↑    ↑    ↑    ↑
            start value a  value b  value c  complete

Meaning of symbols:
-  : Time elapsed (approx. 10ms)
a  : value issued (next)
|  : completion (complete)
#  : error
() : simultaneous issue (a,b)

Practical example 1: map operator ​

Input:  ----1----2----3----|
        map(x => x * 10)
Output: ----10---20---30---|
typescript
import { of } from 'rxjs';
import { map, delay, concatMap } from 'rxjs';

of(1, 2, 3).pipe(
  concatMap(v => of(v).pipe(delay(100))), // pipe every 100ms
  map(x => x * 10)
).subscribe(value => console.log(value));

// 100ms: 10
// 200ms: 20
// 300ms: 30

Practical example 2: merge ​

A:     ----a----b----|
B:     --c----d----e----|
       merge(A, B)
Output:  --c-a--d-b--e----|
typescript
import { interval, merge } from 'rxjs';
import { map, take } from 'rxjs';

const a$ = interval(200).pipe(
  map(i => `A${i}`),
  take(2)
);

const b$ = interval(150).pipe(
  map(i => `B${i}`),
  take(3)
);

merge(a$, b$).subscribe(value => console.log(value));

// 150ms: B0
// 200ms: A0
// 300ms: B1
// 400ms: A1
// 450ms: B2

Practical example 3: switchMap (cancel) ​

Outside:  ----A------B----C----|
          switchMap(x => inner)
inner A:  ----1--2|  (cancel on B)
inner B:         ----3--4|  (canceled by C)
inner C:              ----5--6|
Output:   ----1------3----5--6|
typescript
import { fromEvent, interval } from 'rxjs';
import { switchMap, map, take } from 'rxjs';

const button = document.querySelector('button')!;

fromEvent(button, 'click').pipe(
  switchMap(() =>
    interval(100).pipe(
      map(i => `value${i}`),
      take(3)
    )
  )
).subscribe(value => console.log(value));

// click 1 → value 0 → value 1 → (click 2 to cancel next)
// click 2 → value 0 → value 1 → value 2 → done

Scheduler Role ​

The Scheduler controls when/how the Observable publishes values.

Types of Scheduler ​

SchedulerUsageDescription
queueSchedulerSynchronous processingExecuted immediately at the current event loop
asapSchedulerMicrotasksSame timing as Promise.then()
asyncSchedulerMacro taskSame timing as setTimeout()
animationFrameSchedulerAnimationSame timing as requestAnimationFrame()

Practical example: controlling timing with observeOn ​

❌ Bad example: UI blocked by synchronous process ​

typescript
import { range } from 'rxjs';
import { map } from 'rxjs';

console.log('start');

range(1, 1000000).pipe(
  map(x => x * x)
).subscribe(value => {
  // 1 million calculations are executed synchronously → UI freeze
});

console.log('End'); // output after calculation is finished

✅ Good example: asynchronous with asyncScheduler ​

typescript
import { range, asyncScheduler } from 'rxjs';
import { map, observeOn } from 'rxjs';

console.log('start');

range(1, 1000000).pipe(
  map(x => x * x),
  observeOn(asyncScheduler) // put in async queue
).subscribe(value => {
  // run asynchronously → UI is not blocked
});

console.log('End'); // output immediately

Where to use Scheduler

  • Heavy computation processing: asynchronize with asyncScheduler to avoid blocking the UI
  • Animation: Smooth drawing with animationFrameScheduler
  • Testing: virtualize time with TestScheduler

See Chapter 7: Utilizing Scheduler for details.

Common problems and debugging methods ​

Problem 1: Values do not flow ​

Checklist ​

Debugging technique: using tap ​

typescript
import { of } from 'rxjs';
import { map, filter, tap } from 'rxjs';

console.log('start');

of(1, 2, 3, 4, 5).pipe(
  tap(v => console.log('👁️ Original value:', v)),
  filter(x => x % 2 === 0),
  tap(v => console.log('✅ Passed filter:', v)),
  map(x => x * 10),
  tap(v => console.log('🔄 After conversion by map:', v))
).subscribe(result => {
  console.log('📦 final result:', result);
});

console.log('End');

// Output:
// start
// 👁️ Original value: 1
// 👁️ Original value: 2
// ✅ Passed filter: 2
// 🔄 After conversion by map: 20
// 📦 final result: 20
// 👁️ Original value: 3
// 👁️ Original value: 4
// ✅ Passed filter: 4
// 🔄 After conversion by map: 40
// 📦 final result: 40
// 👁️ Original value: 5
// End

Point

of() is synchronous Observable, so the "End" is output after all processing in subscribe is complete. tap can be inserted between each step to track the flow of values.

Problem 2: Ordering is not as expected ​

❌ Bad example: mergeMap messes up the order ​

typescript
import { of } from 'rxjs';
import { mergeMap, delay } from 'rxjs';

of(1, 2, 3).pipe(
  mergeMap(x =>
    of(x * 10).pipe(
      delay(Math.random() * 100) // random delay
    )
  )
).subscribe(value => console.log(value));

// Example output: 20, 10, 30 (order not guaranteed)

✅ Good example: order guaranteed with concatMap ​

typescript
import { of } from 'rxjs';
import { concatMap, delay } from 'rxjs';

of(1, 2, 3).pipe(
  concatMap(x =>
    of(x * 10).pipe(
      delay(Math.random() * 100)
    )
  )
).subscribe(value => console.log(value));

// Output: 10, 20, 30 (always in that order)

Problem 3: No completion (infinite stream) ​

❌ Bad example: operator stuck waiting for completion ​

typescript
import { interval } from 'rxjs';
import { reduce } from 'rxjs';

interval(1000).pipe(
  reduce((acc, val) => acc + val, 0) // never completes
).subscribe(total => {
  console.log(total); // this line will not be executed
});

✅ Good example: take to separate ​

typescript
import { interval } from 'rxjs';
import { reduce, take } from 'rxjs';

interval(1000).pipe(
  take(5),                            // get only 5
  reduce((acc, val) => acc + val, 0)  // sum after completion
).subscribe(total => {
  console.log('total:', total); // outputs "total: 10" after 5 seconds
});

Debugging tools and techniques ​

1. Log output using tap ​

typescript
import { of } from 'rxjs';
import { map, filter, tap } from 'rxjs';

const debug = <T>(label: string) => tap<T>(value =>
  console.log(`[${label}]`, value)
);

of(1, 2, 3, 4, 5).pipe(
  debug('🔵 input'),
  filter(x => x > 2),
  debug('🟢 after filter'),
  map(x => x * 10),
  debug('🟡 after map')
).subscribe();

// [🔵 input] 1
// [🔵 input] 2
// [🔵 input] 3
// [🟢 after filter] 3
// [🟡 after map] 30
// [🔵 input] 4
// [🟢 after filter] 4
// [🟡 after map] 40
// [🔵 input] 5
// [🟢 after filter] 5
// [🟡 after map] 50

2. RxJS DevTools (browser extension) ​

The Chrome/Edge extension "RxJS DevTools" allows you to:

  • Real-time monitoring of all Observables
  • Visualization in Marble Diagram
  • Track subscribe/unsubscribe

How to install ​

  1. Search for "RxJS DevTools" in the Chrome Web Store
  2. Add the extension
  3. Open the "RxJS" tab in DevTools

3. Custom debug operator ​

typescript
import { interval, map, take, tap, timestamp } from "rxjs";
import { MonoTypeOperatorFunction } from 'rxjs';


function debugWithTime<T>(label: string): MonoTypeOperatorFunction<T> {
  return source => source.pipe(
    timestamp(),
    tap(({ value, timestamp }) => {
      console.log(`[${label}] ${new Date(timestamp).toISOString()}:`, value);
    }),
    map(({ value }) => value)
  );
}

// Usage
interval(500).pipe(
  take(3),
  debugWithTime('⏰ timer'),
  map(x => x * 10),
  debugWithTime('🔄 after conversion')
).subscribe();

// [⏰ timer] 2025-10-19T10:20:59.467Z: 0
// [🔄 after conversion] 2025-10-19T10:20:59.467Z: 0
// [⏰ timer] 2025-10-19T10:20:59.967Z: 1
// [🔄 after conversion] 2025-10-19T10:20:59.967Z: 10
// [⏰ timer] 2025-10-19T10:21:00.467Z: 2
// [🔄 after conversion] 2025-10-19T10:21:00.468Z: 20

4. Marble Testing (testing verification) ​

typescript
import { TestScheduler } from 'rxjs/testing';
import { map } from 'rxjs';

describe('timing testing', () => {
  let scheduler: TestScheduler;

  beforeEach(() => {
    scheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
  });

  it('map converts values', () => {
    scheduler.run(({ cold, expectObservable }) => {
      const input$  = cold('--a--b--c--|', { a: 1, b: 2, c: 3 });
      const expected =     '--x--y--z--|';
      const result$ = input$.pipe(map(v => v * 10));

      expectObservable(result$).toBe(expected, { x: 10, y: 20, z: 30 });
    });
  });
});

See Chapter 9: Marble Testing for details.

Comprehension Checklist ​

Please check if you can answer the following questions.

markdown
## Basic Understanding
- [ ] Explain the difference between synchronous and asynchronous Observable
- [ ] Read the basic notation (-, a, |, #) in the Marble Diagram
- [ ] Understand that values cannot flow without subscribe

## Timing Control
- [ ] Explain the difference between delay, debounceTime, and throttleTime
- [ ] Understand the role of Scheduler
- [ ] Explain the difference between observeOn and subscribeOn

## Debugging
- [ ] Debug the flow of values using tap
- [ ] Identify why values do not flow
- [ ] Know what to do if the order is different than expected

## Practice
- [ ] Separate an infinite Observable with take
- [ ] Implement the difference between mergeMap and concatMap order
- [ ] Can control the timing of errors with catchError

Next Steps ​

Once you understand timing and sequencing, the next step is to learn about state management and sharing.

→ State management difficulties - Subject, share/shareReplay usage

🎯 Practice Problems ​

Problem 1: Discriminating between synchronous and asynchronous ​

Is the following Observable synchronous or asynchronous?

typescript
// A
of(1, 2, 3)

// B
from([1, 2, 3])

// C
of(1, 2, 3).pipe(delay(0))

// D
Promise.resolve(42)

// E
interval(1000).pipe(take(3))
Solution
  • A: Synchronous - of publishes the value immediately
  • B: Synchronous - from expands array immediately
  • C: Asynchronous - delay(0) also queues asynchronously
  • D: Asynchronous - Promise is always asynchronous
  • E: Asynchronous - interval is timer-based

Point

delay(0) and Promise are treated as asynchronous even if the delay is 0 ms.

Problem 2: Reading a Marble Diagram ​

Predict the output of the following Marble Diagram.

typescript
import { of, zip } from 'rxjs';
import { delay } from 'rxjs';

const a$ = of(1, 2, 3);
const b$ = of('A', 'B', 'C').pipe(delay(100));

zip(a$, b$).subscribe(console.log);
Marble Diagram:
a$:  (123)|
b$:  -----(ABC)|
     zip(a$, b$)
Output: ?
Solution
typescript
// output at once after 100ms:
[1, 'A']
[2, 'B']
[3, 'C']

Reason

zip waits until values are available from both streams, so it will not output until delay(100) on b$ is released. a$ issues values synchronously, but waits for b$ before making pairs.

Problem 3: Order Guarantee ​

In the following code, which operator should I use if I want to guarantee the output order?

typescript
import { of } from 'rxjs';
import { mergeMap, delay } from 'rxjs';

of('A', 'B', 'C').pipe(
  mergeMap(letter =>
    of(`${letter} complete`).pipe(
      delay(Math.random() * 100)
    )
  )
).subscribe(console.log);

// current output: random order (e.g., B complete, A complete, C complete)
// expected output: A complete, B complete, C complete
Solution

Modified code:

typescript
import { of } from 'rxjs';
import { concatMap, delay } from 'rxjs';

of('A', 'B', 'C').pipe(
  concatMap(letter =>  // mergeMap → concatMap
    of(`${letter} complete`).pipe(
      delay(Math.random() * 100)
    )
  )
).subscribe(console.log);

// Output: A complete, B complete, C complete (always in that order)

Reason

  • mergeMap: parallel execution, so completion order is not guaranteed
  • concatMap: sequential execution, output in the same order as input

Problem 4: Handling infinite streams ​

Please point out the problem in the following code and correct it.

typescript
import { interval } from 'rxjs';
import { map, toArray } from 'rxjs';

interval(1000).pipe(
  map(x => x * 2),
  toArray()
).subscribe(arr => {
  console.log('Array:', arr); // will this line be executed?
});
Solution

Problem:

  • interval issues values indefinitely, so it never completes
  • toArray() waits for completion signal, so no value is issued forever

Fixed code:

typescript
import { interval } from 'rxjs';
import { map, take, toArray } from 'rxjs';

interval(1000).pipe(
  take(5),          // get only 5 and done
  map(x => x * 2),
  toArray()
).subscribe(arr => {
  console.log('Array:', arr); // [0, 2, 4, 6, 8]
});

Point

When using reduce, toArray, last, or other "wait for completion operators" for an infinite stream, you must always separate them with take, first, takeUntil, etc.

Lançado sob a licença CC-BY-4.0.