Reactive programming with RxJS can be powerful, but debugging asynchronous data streams can get tricky. This notebook provides practical techniques for testing and debugging RxJS streams to help you troubleshoot issues and write reliable tests.
$ npm install rxjs added 2 packages in 2s 28 packages are looking for funding run `npm fund` for details
RxJS loaded successfully
The tap()
operator is your best friend for debugging. It lets you peek into the stream without changing it.
Debugging with tap(): Original value: 1 After map: 10 Original value: 2 After map: 20 Original value: 3 After map: 30 After filter: 30 Final value: 30 Original value: 4 After map: 40 After filter: 40 Final value: 40 Original value: 5 After map: 50 After filter: 50 Final value: 50 Stream completed
Let's create a more powerful debug operator to consistently log stream values with context.
Using custom debug operator: [Source] Next: {"id":1,"name":"Alice"} [After uppercase] Next: {"id":1,"name":"ALICE"} [Source] Next: {"id":2,"name":"Bob"} [After uppercase] Next: {"id":2,"name":"BOB"} [After filter] Next: {"id":2,"name":"BOB"} [Source] Next: {"id":3,"name":"Charlie"} [After uppercase] Next: {"id":3,"name":"CHARLIE"} [After filter] Next: {"id":3,"name":"CHARLIE"} [Source] Complete [After uppercase] Complete [After filter] Complete
Tracking when subscriptions are created and disposed is critical for debugging memory leaks.
Tracking subscription: Timer stream Subscribing to Timer stream... Timer stream emitted: 0 Timer stream emitted: 1 Timer stream emitted: 2 Stream Timer stream finalized
Error handling is a critical part of working with RxJS.
Error handling demo: 1. Without error handling: [No error handling] Next: 1 Received: 1 [No error handling] Next: 2 Received: 2 [No error handling] Next: 3 Received: 3 [No error handling] Error: Error: Simulated error! Error caught in subscriber: Simulated error! 2. With catchError for recovery: [Before catchError] Next: 1 [After catchError] Next: 1 [Before catchError] Next: 2 [After catchError] Next: 2 [Before catchError] Next: 3 [After catchError] Next: 3 [Before catchError] Error: Error: Simulated error! Error caught by operator: Simulated error! [After catchError] Next: "Fallback value" [After catchError] Complete Stream completed with recovery
Marble testing is a powerful technique for testing RxJS streams. It uses ASCII diagrams to represent stream events over time.
Basic marble testing example: Actual: [ { "frame": 2, "notification": { "kind": "N", "value": 10 } }, { "frame": 5, "notification": { "kind": "N", "value": 20 } }, { "frame": 8, "notification": { "kind": "C" } } ] Expected: [ { "frame": 2, "notification": { "kind": "N", "value": 10 } }, { "frame": 5, "notification": { "kind": "N", "value": 20 } }, { "frame": 8, "notification": { "kind": "C" } } ] Test passed: true
Testing async operators with marble diagrams: Actual: [ { "frame": 15, "notification": { "kind": "N", "value": 3 } }, { "frame": 18, "notification": { "kind": "C" } } ] Expected: [ { "frame": 3, "notification": { "kind": "N", "value": 2 } }, { "frame": 5, "notification": { "kind": "N", "value": 3 } }, { "frame": 10, "notification": { "kind": "C" } } ] Test passed: false
Memory leaks are a common issue with RxJS. Let's look at how to detect and fix them.
Memory leak detection: Creating a potential memory leak... Creating a properly managed stream... Leak emitted: 0 Safe stream emitted: 0 Leak emitted: 1 Safe stream emitted: 1 Cleaning up both streams... Stream properly cleaned up
Add debug operators strategically:
tap()
calls before and after complex operatorsAlways handle errors:
catchError
for recoveryPrevent memory leaks:
takeUntil(destroy$)
pattern in componentsfinalize()
to verify cleanupWrite testable streams:
Use TestScheduler for time-based testing:
'--a--b--|'
expectObservable(result$).toBe('--x--y--|')
Avoid nested subscriptions:
switchMap
, mergeMap
insteadTest edge cases: