Skip to content

Debugging Barrier ​

When you face problems in RxJS like "values don't flow", "values are different than expected", or "there might be a memory leak", it takes time to resolve without knowing proper debugging methods. This page comprehensively explains RxJS-specific debugging techniques.

Basic RxJS Debugging Strategy ​

5 Steps of Debugging ​

Step 1: Identify the Problem ​

First, clarify what the problem is.

SymptomPossible Causes
No values flow at allForgot subscribe, ended before complete, excluded by filter
First value doesn't appearcombineLatest initial condition not met, BehaviorSubject not set
Order is wrongUsing mergeMap, asynchronous timing
Duplicate values appearMultiple subscribes without share, shareReplay misuse
Memory leakForgot unsubscribe, shareReplay refCount: false
Values are delayeddebounceTime, throttleTime, asynchronous processing

Step 2: Form a Hypothesis ​

Guess the cause of the problem.

typescript
// Example: "values don't appear" problem
// Hypothesis 1: Not subscribed?
// Hypothesis 2: complete/error too early?
// Hypothesis 3: Excluded by filter?
// Hypothesis 4: Taking time due to async?

Step 3: Check with tap ​

Insert tap at each stage to confirm what's actually happening.

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

of(1, 2, 3, 4, 5).pipe(
  tap(v => console.log('🔵 Input:', v)),
  filter(x => x > 10), // ❌ All excluded
  tap(v => console.log('✅ Passed filter:', v)),
  map(x => x * 10),
  tap(v => console.log('🟢 After map:', v))
).subscribe(result => {
  console.log('📦 Result:', result);
});

// Output:
// 🔵 Input: 1
// 🔵 Input: 2
// 🔵 Input: 3
// 🔵 Input: 4
// 🔵 Input: 5
// (None passed filter → filter is the cause)

Common Debugging Scenarios ​

Scenario 1: Values Don't Flow ​

Problem 1-1: Forgot subscribe ​

❌ Bad Example: Not subscribed ​

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

const result$ = of(1, 2, 3).pipe(
  map(x => x * 10)
);

console.log('Complete'); // Outputs immediately
// No values flow to result$ (because not subscribed)

✅ Good Example: Subscribe ​

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

const result$ = of(1, 2, 3).pipe(
  map(x => x * 10)
);

result$.subscribe(value => {
  console.log('Value:', value);
});

console.log('Complete');

// Output:
// Value: 10
// Value: 20
// Value: 30
// Complete

Checkpoint

  • Nothing happens just by defining an Observable
  • Must subscribe
  • No need to subscribe when using async pipe (Angular, etc.)

Problem 1-2: complete/error Too Early ​

❌ Bad Example: complete comes first ​

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

EMPTY.pipe( // ❌ Completes immediately
  map(x => x * 10)
).subscribe({
  next: value => console.log('Value:', value),
  complete: () => console.log('Complete')
});

// Output:
// Complete
// (No values flow)

✅ Good Example: Check with tap ​

typescript
import { EMPTY } from 'rxjs';
import { map, tap } from 'rxjs';

EMPTY.pipe(
  tap(() => console.log('👁️ Value arrived')), // This doesn't output
  map(x => x * 10)
).subscribe({
  next: value => console.log('Value:', value),
  complete: () => console.log('Complete')
});

// Output:
// Complete
// (tap also not executed → EMPTY is the cause)

Problem 1-3: Excluded by filter ​

❌ Bad Example: All excluded unknowingly ​

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

of(1, 2, 3, 4, 5).pipe(
  filter(x => x > 100) // ❌ All excluded
).subscribe(value => {
  console.log('Value:', value); // Nothing output
});

✅ Good Example: Check with tap ​

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

of(1, 2, 3, 4, 5).pipe(
  tap(v => console.log('Before filter:', v)),
  filter(x => x > 100),
  tap(v => console.log('After filter:', v)) // None output
).subscribe(value => {
  console.log('Value:', value);
});

// Output:
// Before filter: 1
// Before filter: 2
// Before filter: 3
// Before filter: 4
// Before filter: 5
// (None after filter → filter is too strict)

Scenario 2: Values Different Than Expected ​

Problem 2-1: Type Conversion Mistake ​

❌ Bad Example: String and number confusion ​

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

const input = '5'; // String

of(input).pipe(
  map(x => x + 10) // ❌ '5' + 10 = '510' (string concatenation)
).subscribe(result => {
  console.log('Result:', result); // Result: 510
  console.log('Type:', typeof result); // Type: string
});

✅ Good Example: Check type with tap ​

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

const input = '5';

of(input).pipe(
  tap(x => console.log('Input:', x, typeof x)),
  map(x => Number(x)), // Convert to number
  tap(x => console.log('After conversion:', x, typeof x)),
  map(x => x + 10)
).subscribe(result => {
  console.log('Result:', result); // Result: 15
});

Problem 2-2: Asynchronous Order ​

❌ Bad Example: Order disrupted with mergeMap ​

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

of(1, 2, 3).pipe(
  mergeMap(x =>
    of(x * 10).pipe(
      delay(Math.random() * 1000) // Random delay
    )
  )
).subscribe(value => {
  console.log('Value:', value);
});

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

✅ Good Example: Guarantee order with concatMap ​

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

of(1, 2, 3).pipe(
  tap(x => console.log('Input:', x)),
  concatMap(x =>
    of(x * 10).pipe(
      delay(Math.random() * 1000),
      tap(v => console.log('Complete:', v))
    )
  )
).subscribe(value => {
  console.log('Value:', value);
});

// Output (always this order):
// Input: 1
// Complete: 10
// Value: 10
// Input: 2
// Complete: 20
// Value: 20
// Input: 3
// Complete: 30
// Value: 30

Scenario 3: Detecting Memory Leaks ​

Problem 3-1: Forgot unsubscribe ​

❌ Bad Example: Don't unsubscribe ​

typescript
import { interval } from 'rxjs';

class Component {
  ngOnInit() {
    interval(1000).subscribe(n => {
      console.log('Value:', n); // Runs forever
    });
  }

  ngOnDestroy() {
    // Not unsubscribing → Memory leak
  }
}

✅ Good Example: Auto-release with takeUntil ​

typescript
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs';

class Component {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    interval(1000).pipe(
      takeUntil(this.destroy$)
    ).subscribe(n => {
      console.log('Value:', n);
    });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
    console.log('Unsubscribe complete');
  }
}

Problem 3-2: shareReplay Memory Leak ​

❌ Bad Example: Leak with refCount: false ​

typescript
import { interval } from 'rxjs';
import { shareReplay, take, tap } from 'rxjs';

const data$ = interval(1000).pipe(
  take(100),
  tap(n => console.log('Generate:', n)),
  shareReplay({ bufferSize: 1, refCount: false })
  // ❌ refCount: false → Runs forever
);

const sub = data$.subscribe(n => console.log('Subscribe 1:', n));

setTimeout(() => {
  sub.unsubscribe();
  console.log('Unsubscribed but internally still running');
}, 5000);

✅ Good Example: Auto-stop with refCount: true ​

typescript
import { interval } from 'rxjs';
import { shareReplay, take, tap } from 'rxjs';

const data$ = interval(1000).pipe(
  take(100),
  tap(n => console.log('Generate:', n)),
  shareReplay({ bufferSize: 1, refCount: true })
  // ✅ refCount: true → Stops when all unsubscribe
);

const sub = data$.subscribe(n => console.log('Subscribe 1:', n));

setTimeout(() => {
  sub.unsubscribe();
  console.log('Unsubscribe → Stream also stops');
}, 5000);

Debugging Tools and Techniques ​

1. Stepwise Debugging with tap ​

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

const debugTap = <T>(label: string, color: string = '🔵') =>
  tap<T>({
    next: value => console.log(`${color} [${label}] next:`, value),
    error: error => console.error(`❌ [${label}] error:`, error),
    complete: () => console.log(`✅ [${label}] complete`)
  });

of(1, 2, 3, 4, 5).pipe(
  debugTap('Input'),
  filter(x => x % 2 === 0),
  debugTap('After filter', '🟢'),
  map(x => x * 10),
  debugTap('After map', '🟡')
).subscribe({
  next: value => console.log('📦 Final result:', value),
  complete: () => console.log('🏁 Complete')
});

2. Custom Debug Operator ​

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

interface DebugOptions {
  label: string;
  showTimestamp?: boolean;
  showDiff?: boolean;
}

let lastTimestamp = 0;

function debug<T>(options: DebugOptions): MonoTypeOperatorFunction<T> {
  const { label, showTimestamp = true, showDiff = true } = options;

  return source => source.pipe(
    timestamp(),
    tap(({ value, timestamp }) => {
      const parts = [`[${label}]`, value];

      if (showTimestamp) {
        parts.push(`@${new Date(timestamp).toISOString()}`);
      }

      if (showDiff && lastTimestamp > 0) {
        const diff = timestamp - lastTimestamp;
        parts.push(`(+${diff}ms)`);
      }

      console.log(...parts);
      lastTimestamp = timestamp;
    }),
    map(({ value }) => value)
  );
}

// Usage
import { interval } from 'rxjs';
import { take } from 'rxjs';

interval(500).pipe(
  take(5),
  debug({ label: 'Timer' }),
  map(x => x * 10),
  debug({ label: 'After conversion', showDiff: false })
).subscribe();

3. RxJS DevTools (Browser Extension) ​

Installation:

  1. Search "RxJS DevTools" in Chrome/Edge Web Store
  2. Add extension
  3. Open DevTools and click "RxJS" tab

Main Features:

  • Monitor all Observables in real-time
  • Visualization with Marble Diagrams
  • Track subscribe/unsubscribe
  • Performance analysis

Usage Example:

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

// Automatically detected by DevTools
const timer$ = interval(1000).pipe(
  take(10),
  map(x => x * 2)
);

timer$.subscribe(value => console.log(value));

4. Debugging Errors ​

Identify Error Location ​

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

of(1, 2, 3).pipe(
  tap(v => console.log('1. Input:', v)),
  map(x => {
    if (x === 2) {
      throw new Error('Cannot use 2');
    }
    return x * 10;
  }),
  tap(v => console.log('2. After map:', v)), // Not executed on error
  catchError(error => {
    console.error('3. Error caught:', error.message);
    return of(-1); // Return default value
  }),
  tap(v => console.log('4. After catchError:', v))
).subscribe({
  next: value => console.log('5. Result:', value),
  error: error => console.error('Subscribe error:', error),
  complete: () => console.log('6. Complete')
});

// Output:
// 1. Input: 1
// 2. After map: 10
// 5. Result: 10
// 1. Input: 2
// 3. Error caught: Cannot use 2
// 4. After catchError: -1
// 5. Result: -1
// 6. Complete

Performance Debugging ​

Problem 1: Excessive Recalculation ​

❌ Bad Example: Frequent recalculation with combineLatest ​

typescript
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs';

const a$ = new BehaviorSubject(1);
const b$ = new BehaviorSubject(2);
const c$ = new BehaviorSubject(3);

combineLatest([a$, b$, c$]).pipe(
  map(([a, b, c]) => {
    console.log('Heavy calculation executed'); // Executed frequently
    return a + b + c;
  })
).subscribe(result => console.log('Result:', result));

// Frequent updates
setInterval(() => {
  a$.next(Math.random());
}, 100);

✅ Good Example: Exclude duplicates with distinctUntilChanged ​

typescript
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map, distinctUntilChanged } from 'rxjs';

const a$ = new BehaviorSubject(1);
const b$ = new BehaviorSubject(2);
const c$ = new BehaviorSubject(3);

combineLatest([a$, b$, c$]).pipe(
  map(([a, b, c]) => Math.floor(a) + Math.floor(b) + Math.floor(c)),
  distinctUntilChanged(), // Pass only when value changes
  map(sum => {
    console.log('Heavy calculation executed'); // Only when value changes
    return sum * 2;
  })
).subscribe(result => console.log('Result:', result));

setInterval(() => {
  a$.next(Math.random());
}, 100);

Problem 2: Monitor Memory Usage ​

typescript
import { interval } from 'rxjs';
import { scan, tap } from 'rxjs';

let itemCount = 0;

interval(100).pipe(
  scan((acc, val) => {
    acc.push(val);
    itemCount = acc.length;
    return acc;
  }, [] as number[]),
  tap(() => {
    if (itemCount % 100 === 0) {
      console.log(`Item count: ${itemCount}`);
      if (itemCount > 10000) {
        console.warn('⚠️ Memory usage too high');
      }
    }
  })
).subscribe();

Problem 3: Monitor Subscription Count ​

typescript
import { Observable, Subject } from 'rxjs';

class MonitoredSubject<T> extends Subject<T> {
  private subscriptionCount = 0;

  subscribe(...args: any[]): any {
    this.subscriptionCount++;
    console.log(`Subscription count: ${this.subscriptionCount}`);

    const subscription = super.subscribe(...args);

    const originalUnsubscribe = subscription.unsubscribe.bind(subscription);
    subscription.unsubscribe = () => {
      this.subscriptionCount--;
      console.log(`Subscription count: ${this.subscriptionCount}`);
      originalUnsubscribe();
    };

    return subscription;
  }
}

// Usage
const data$ = new MonitoredSubject<number>();

const sub1 = data$.subscribe(v => console.log('Subscribe 1:', v));
const sub2 = data$.subscribe(v => console.log('Subscribe 2:', v));

sub1.unsubscribe();
sub2.unsubscribe();

// Output:
// Subscription count: 1
// Subscription count: 2
// Subscription count: 1
// Subscription count: 0

Debugging Checklist ​

When a problem occurs, check the following in order.

markdown
## Basic Check
- [ ] Calling `subscribe()`?
- [ ] `complete` or `error` not too early?
- [ ] Values not excluded by `filter` or `take`?
- [ ] Waiting for asynchronous processing completion?

## Timing Check
- [ ] Understanding synchronous/asynchronous?
- [ ] Confirmed impact of `delay`, `debounceTime`, `throttleTime`?
- [ ] Met `combineLatest` initial firing condition?

## Memory Check
- [ ] Using `unsubscribe` or `takeUntil`?
- [ ] Set `refCount: true` for `shareReplay`?
- [ ] Properly limiting infinite Observables?

## Performance Check
- [ ] Excessive recalculation not occurring? (consider `distinctUntilChanged`)
- [ ] Subscription count not too high?
- [ ] Heavy processing made asynchronous with `observeOn(asyncScheduler)`?

Comprehension Checklist ​

Check if you can answer the following questions.

markdown
## Basic Debugging
- [ ] Can debug value flow using tap
- [ ] Can identify error location
- [ ] Can confirm complete/error timing

## Tool Usage
- [ ] Know basic RxJS DevTools usage
- [ ] Can create custom debug operators
- [ ] Can measure timing with timestamp

## Problem Solving
- [ ] Can identify causes of values not flowing
- [ ] Can find memory leak signs
- [ ] Can identify performance problems

## Prevention
- [ ] Have habit of stepwise debugging with tap
- [ ] Properly implementing error handling
- [ ] Know memory leak countermeasures

Next Steps ​

Once you understand debugging techniques, integrate all the knowledge you've learned and study practical patterns.

→ Chapter 13: Practical Patterns (in preparation) - Patterns for practical use

🎯 Exercises ​

Problem 1: Identifying Why Values Don't Flow ​

In the following code, identify why values are not output.

typescript
import { Subject, combineLatest } from 'rxjs';

const a$ = new Subject<number>();
const b$ = new Subject<number>();

combineLatest([a$, b$]).subscribe(([a, b]) => {
  console.log('Values:', a, b);
});

a$.next(1);
console.log('Complete');
Solution

Cause

combineLatest doesn't fire until all streams have emitted at least once

Since b$ hasn't emitted a value yet, just a$.next(1) won't fire.

Fix Method 1: Emit value to b$ too ​

typescript
import { Subject, combineLatest } from 'rxjs';

const a$ = new Subject<number>();
const b$ = new Subject<number>();

combineLatest([a$, b$]).subscribe(([a, b]) => {
  console.log('Values:', a, b);
});

a$.next(1);
b$.next(2); // ← Fires here
console.log('Complete');

// Output:
// Values: 1 2
// Complete

Fix Method 2: Use BehaviorSubject ​

typescript
import { BehaviorSubject, combineLatest } from 'rxjs';

const a$ = new BehaviorSubject<number>(0); // Initial value
const b$ = new BehaviorSubject<number>(0);

combineLatest([a$, b$]).subscribe(([a, b]) => {
  console.log('Values:', a, b);
});

// Output: Values: 0 0 (fires immediately)

a$.next(1);
// Output: Values: 1 0

Debugging Technique

Using tap to check each stream's values shows where it's stuck.

typescript
a$.pipe(tap(v => console.log('a$:', v)))
b$.pipe(tap(v => console.log('b$:', v)))

Problem 2: Fixing Memory Leak ​

The following code has a memory leak. Fix it.

typescript
import { interval } from 'rxjs';
import { Component } from '@angular/core';

class MyComponent implements Component {
  ngOnInit() {
    interval(1000).subscribe(n => {
      console.log('Timer:', n);
    });
  }

  ngOnDestroy() {
    console.log('Destroy');
  }
}
Solution

Problem

Not unsubscribing in ngOnDestroy, so interval continues even after component is destroyed

Fix Method 1: Save Subscription and unsubscribe ​

typescript
import { interval, Subscription } from 'rxjs';

class MyComponent {
  private subscription!: Subscription;

  ngOnInit() {
    this.subscription = interval(1000).subscribe(n => {
      console.log('Timer:', n);
    });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
    console.log('Destroy & unsubscribe');
  }
}
typescript
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs';

class MyComponent {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    interval(1000).pipe(
      takeUntil(this.destroy$)
    ).subscribe(n => {
      console.log('Timer:', n);
    });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
    console.log('Destroy & unsubscribe');
  }
}

Key Points

  • Infinite Observables like interval always require unsubscribe
  • takeUntil pattern is recommended (can manage multiple subscriptions at once)
  • In Angular, using async pipe automatically unsubscribes

Problem 3: Order Problem ​

In the following code, explain why order is not guaranteed and fix it.

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

from([1, 2, 3]).pipe(
  mergeMap(x =>
    of(x).pipe(
      delay(Math.random() * 1000)
    )
  )
).subscribe(value => console.log(value));

// Example output: 2, 1, 3 (order not guaranteed)
Solution

Problem

mergeMap executes in parallel, so completion order depends on execution time

Fix Method: Use concatMap ​

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

from([1, 2, 3]).pipe(
  tap(x => console.log('Start:', x)),
  concatMap(x =>
    of(x).pipe(
      delay(Math.random() * 1000),
      tap(v => console.log('Complete:', v))
    )
  )
).subscribe(value => console.log('Result:', value));

// Output (always this order):
// Start: 1
// Complete: 1
// Result: 1
// Start: 2
// Complete: 2
// Result: 2
// Start: 3
// Complete: 3
// Result: 3

Reason

  • mergeMap: Parallel execution, completion order not guaranteed
  • concatMap: Sequential execution, always output in same order as input
  • switchMap: Latest only, old processing is cancelled
  • exhaustMap: Ignore new processing while executing

Comparison with Marble Diagram ​

Input:  --1--2--3----|

mergeMap:  --2--1--3--|  (completion order)
concatMap: --1--2--3-| (input order)

Problem 4: Performance Improvement ​

The following code has frequent recalculation. Improve performance.

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

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

fromEvent(input, 'input').pipe(
  map(e => (e.target as HTMLInputElement).value),
  map(value => {
    console.log('Heavy calculation executed');
    return value.toUpperCase();
  })
).subscribe(result => console.log(result));

// User types "hello"
// Heavy calculation executed (h)
// Heavy calculation executed (he)
// Heavy calculation executed (hel)
// Heavy calculation executed (hell)
// Heavy calculation executed (hello)
Solution

Improvement Method 1: Wait for input completion with debounceTime ​

typescript
import { fromEvent } from 'rxjs';
import { map, debounceTime } from 'rxjs';

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

fromEvent(input, 'input').pipe(
  debounceTime(300), // Execute if no input for 300ms
  map(e => (e.target as HTMLInputElement).value),
  map(value => {
    console.log('Heavy calculation executed');
    return value.toUpperCase();
  })
).subscribe(result => console.log(result));

// Type "hello" and wait 300ms, executes only once

Improvement Method 2: Exclude duplicates with distinctUntilChanged ​

typescript
import { fromEvent } from 'rxjs';
import { map, debounceTime, distinctUntilChanged } from 'rxjs';

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

fromEvent(input, 'input').pipe(
  debounceTime(300),
  map(e => (e.target as HTMLInputElement).value),
  distinctUntilChanged(), // Ignore if same as previous value
  map(value => {
    console.log('Heavy calculation executed');
    return value.toUpperCase();
  })
).subscribe(result => console.log(result));

Performance Improvement Techniques

  • debounceTime: Wait for input completion
  • throttleTime: Thin out at regular intervals
  • distinctUntilChanged: Exclude duplicates
  • observeOn(asyncScheduler): Make heavy processing asynchronous
  • shareReplay: Cache results

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