RxJs is the most challenging library of the Angular ecosystem because of its syntax, numerous operators, and the asynchronous mindset associated with Observables.

Today, let’s see the easiest way to debug RxJs code in Angular. Now, you’ve probably done something like this at least once:

service.getSomeObservable().subscribe(data => {
console.log(data);
});

While the above works, it’s not ideal for a bunch of reasons:

  1. It forces you to subscribe to an Observable instead of using the async pipe, which is much better.
  2. It doesn’t allow you to debug what happens in that Observable before the subscriber receives the data.

For instance, let’s say you inherit the following code to debug and understand:

this.continent$ = this.continentSelect.valueChanges.pipe(
withLatestFrom(country$),
map(([continent, countries]) => [
continent,
countries.filter((c) => c.continent == continent),
]),
tap(([continent, filteredCountries]) => {
this.countries = filteredCountries;
this.countrySelect.setValue(filteredCountries[0].country);
}),
map(([continent]) => continent.substring(0, 3).toUpperCase())
);

There are four different operators here, and it would be helpful to visualize what’s happening at each step.

The good news is that there’s an operator for that, and it’s called tap. Tap is perfect for looking at what’s going on inside an Observable chain. I like to say it’s a “spy” operator.

If we want to debug our previous code, we can add the following line of code between our different operators:

tap(data => console.log(data))

That works, but it’s still verbose. Tap takes a function as a parameter and passes our data as a parameter to that function. As a result, we can simplify our debugging code into:

tap(console.log)

And then apply it to our complex chain of operators:

this.continent$ = this.continentSelect.valueChanges.pipe(
tap(console.log),
withLatestFrom(country$),
tap(console.log),
map(([continent, countries]) => [
continent,
countries.filter((c) => c.continent == continent),
]),
tap(console.log),
tap(([continent, filteredCountries]) => {
this.countries = filteredCountries;
this.countrySelect.setValue(filteredCountries[0].country);
}),
tap(console.log),
map(([continent]) => continent.substring(0, 3).toUpperCase())
);

Now I can see that when I receive the value “Europe,” it gets combined with a list of all countries, then the list of countries gets filtered, and in the end, we receive the continent’s code as a 3-letter uppercase string:

Note that if you don’t want to repeat yourself, you can create your function as a custom operator.

export function debug() {
return tap(console.log);
}

One benefit here is that you could use that function to turn off logging in production mode:

export function debug() {
if (environment.production) {
return tap();
} else {
return tap(console.log);
}

And then our debuggable code becomes:

this.continent$ = this.continentSelect.valueChanges.pipe(
debug(),
withLatestFrom(country$),
debug(),
map(([continent, countries]) => [
continent,
countries.filter((c) => c.continent == continent),
]),
debug(),
tap(([continent, filteredCountries]) => {
this.countries = filteredCountries;
this.countrySelect.setValue(filteredCountries[0].country);
}),
debug(),
map(([continent]) => continent.substring(0, 3).toUpperCase())
);

Of course, you can also use breakpoints and other tactics, but due to the asynchronous nature of most things in RxJs, breakpoints can slow things down to the point that your debugging session isn’t realistic anymore.

You can test my example on Stackblitz.

Let me know if you use other fun tactics to debug your RxJs code!

My name is Alain Chautard. I am a Google Developer Expert in Angular and a consultant and trainer at Angular Training, where I help web development teams learn and become comfortable with Angular.

If you enjoyed this article, please clap for it or share it. Your help is always appreciated. You can also subscribe to my articles and the Weekly Angular Newsletter for helpful Angular tips, updates, and tutorials.

Share.
Leave A Reply