Before We Start
One of the more difficult things to master when learning RxJS is learning how to debug a stream. Within regular Typescript, debugging is relatively easy. I find myself alternating between two different methods when following this approach:
1. Very simply using console.log
2. Using DevTool Sources
Using Console.log
In a regular Typescript/Javascript setting a console.log is a relatively simple concept. We show(,or log out), whatever the current status is of our code at a given time. So for instance, let’s say that we wanted to create a piece of code that takes the first and last name, and combines(proper software term is concatenates) them together. Something like this:
combinedName(firstName: string , lastName: string): string {
return firstName + lastName;
}
So the developer, due to the low level of the logic of this function, mistakenly forgets to add white space in between. He decides to console out the function, before applying the logic directly to the application, to make sure it works as expected. So the developer does something like this(assuming we are talking about a method within a service):
console.log(this.combinedName('James', 'Harden));
The developer is expecting it to look like ‘James Harden’
, but instead, the developer finds out that it is ’JamesHarden’
. The developer immediately realizes that there is a missing white space in between the two words, and edits the function, so that it works as expected:
combinedName(firstName: string , lastName: string): string {
return firstName + ' ' + lastName;
}
This individual then goes back, and refreshes the page where the console.log was, and finds out that it now looks like:
'James Harden'
In practice, something like this should ideally be tested using unit tests. However, many enterprise applications unfortunately are not built in the optimal fashion for various reasons.
Using DevTool Sources
The only other tool that I usually have within my tool belt to dissect an error immediately is the developer tools within chrome. Any time that there is an error, there is a unique signature that your code will throw out. In a modern day application, this will happen even if it isn’t unique to your application, due to the number of libraries/frameworks we use. However, it can still be incredibly useful for hard to find bugs, if you are willing to dig deep enough into the call stack.
This article will not discuss this technique in-depth. Feel free to check out the video series by egghead.io here, for more details. I just wanted to bring this up, as an integral part of day to day debugging.
Debugging within RxJS — The Dilemma
The immediate issue with debugging RxJS, coming from a traditional setting, is that it’s only the final result that is outputted, but that is not the case. One might think, it is therefore only the final result that can be debugged. For instance, you might have a stream of numerous chained events like so:
this.companies$ = searchBy$.pipe(
debounceTime(300),
distinctUntilChanged(),
startWith(''),
switchMap((criteria:string) => {
const request$ = this.companyService.searchCompanies(criteria);
return !criteria.length ? of([]) : request$
})
);
In the above, only in the switchMap
, or in a subscribe will we be able to tap into the result that we are looking for. For instance, let’s go through the potential bugs that might happen with the above stream:
1. The result might not appear because it is within the debounceTime
;
2. It might not have changed from the previous result, i.e. distinctUntilChanged
, and therefore is not triggered.
3. The companyService.searchCompanies request might be erroring out, or there might be some internal logic to the companyService.searchCompanies
request making something error out.
So, of course, we need a similar way to console out our results along the line of the stream, to figure out where our error is happening.
Using Tap
tap
is RxJS’s way of taking the current value and outputting it. tap
is colloquially referred to as a “side-effect”. A piece of logic within our RxJS stream, that does not directly output to the stream’s final output. It is commonly used in real-life code, by taking the value returned by an HTTP/GraphQL service, and passing data returned to a function/action. However, tap
can also be used as a debugger. For instance, in the above code, let’s say we aren’t getting the result we wanted. We can insert tap
as follows:
this.companies$ = searchBy$.pipe(
debounceTime(300),
tap(result => console.log(result)),
distinctUntilChanged(),
startWith(''),
switchMap((criteria:string) => {
const request$ = this.companyService.searchCompanies(criteria);
return !criteria.length ? of([]) : request$ }
)
);
In the above tap
, (let’s imagine) a result is coming back from our console.log
. In this scenario, we now know that it is not erroring out before debounceTime
. Ok, so let’s move the tap
down one more:
this.companies$ = searchBy$.pipe(
debounceTime(300),
distinctUntilChanged(),
tap(result => console.log(result)), startWith(''),
switchMap((criteria:string) => {
const request$ = this.companyService.searchCompanies(criteria);
return !criteria.length ? of([]) : request$ })
);
Here (let’s imagine, but using this example for the point of demonstration) we are no longer receiving the console.log
that we want. This is/would be telling us that the reason we might not be receiving our result, is that the value is not being registered as anything distinct.
That’s it plain and simple. Use tap
within your RxJS
stream, in combination with console.log
to be able to debug RxJS code.
I’ve seen one other article in particular claim use of a debug utility function that is only turned on in development mode. I personally am against such debugging patterns and feel it is an anti-pattern. I strongly believe that unit tests should be in place to prevent errors. Debugging should happen in one-off scenarios where unknown business cases or use cases cause an error, fixed and then reinforced with unit tests and integration tests to make sure error does not happen again.
That’s it, you are now a pro at debugging RxJS!!!