Improve: RxJS Debugging
Colum Ferry
Posted on March 23, 2021
Reactive programming in Frontend Web Development makes sense as it's a paradigm focused on reacting to events. The user interacts with the DOM, which broadcasts events.
RxJS is the Swiss Army Knife of Reactive Programming. It gives us a push-based pattern that allows us to respond to events created by the user.
š± But there's one major problem. Debugging RxJS Streams can be a nightmare!
This article will introduce you to a new community-owned RxJS Operator aiming to help streamline debugging and provide the ability to do some additional robust debugging techniques.
š„ The debug()
Operator
What is this magical operator? It's the debug()
operator (you can find out more here) and can be installed pretty easily:
NPM:
npm install rxjs-debug-operator
Yarn:
yarn add rxjs-debug-operator
It's also pretty easy to use! Just pipe it into any stream you already have set up:
obs$.pipe(debug());
š¤ But what does it do?
In short, it helps you figure out what is going wrong, or right, with your RxJS Streams.
By default, it'll simply log values to the console.
const obs$ = of('my test value');
obs$.pipe(debug()).subscribe();
// OUTPUT
// CONSOLE
// my test value
However, it is so much more flexible than that and can be a powerful tool for diagnosing problems or even reporting critical errors in user pathways that could negatively impact the business!
š” How can I take full advantage of it?
There are endless use-cases that you could employ to take advantage of the operator.
A common one is to see how the value changes throughout a series of operations in the stream.
This can be made even easier to work with thanks to the handy label
option:
const obs$ = of('my test value');
obs$
.pipe(
debug('Starting Value'),
map((v) => `Hello this is ${v}`),
debug('Finishing Value')
)
.subscribe();
// OUTPUT
// CONSOLE
// Starting Value my test value
// Finishing Value Hello this is my test value
We'll look at some more specific use cases below!
šø Examples
Hopefully, these examples will come in useful and show the power of the operator!
š Simple logging
Without Label
const obs$ = of('my test value');
obs$.pipe(debug()).subscribe();
// OUTPUT
// CONSOLE
// my test value
With Label
const obs$ = of('my test value');
obs$.pipe(debug('Label')).subscribe();
// OUTPUT
// CONSOLE
// Label my test value
You can even change what happens on each notification:
const obs$ = of('my test value');
obs$
.pipe(
debug({
next: (value) => console.warn(value),
})
)
.subscribe();
// OUTPUT
// WARNING
// my test value
const obs$ = throwError('my error');
obs$
.pipe(
debug({
error: (value) => console.warn(value),
})
)
.subscribe();
// OUTPUT
// WARNING
// my error
š©āš» Only logging in dev mode
You can also only log when in dev mode.
There are two ways you can do this, globally for all instances of debug()
or locally, on a case-by-case basis.
Globally
import { environment } from '@app/env';
setGlobalDebugConfig({ shouldIgnore: !environment.isDev });
const obs$ = of('my test value');
obs$.pipe(debug()).subscribe();
// When environment.isDev === false
// Nothing is output
Locally
import { environment } from '@app/env';
const obs$ = of('my test value');
obs$.pipe(debug({ shouldIgnore: !environment.isDev })).subscribe();
// When environment.isDev === false
// Nothing is output
ā±ļø Measuring the performance of a set of operators
Now for something potentially very cool. We can use the debug operator with custom next
notification handlers to measure the performance/time of a set of piped operations to a Stream.
The example below shows it being used to measure the time it takes for a switchMap
to a network request, however, the approach could be taken with any set of operators.
import { of } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { switchMap } from 'rxjs/operators';
import { debug } from 'rxjs-debug-operator';
const obs$ = of('my test value');
obs$
.pipe(
debug({
next: () => console.time('Measure Perf'),
}),
switchMap((ar) =>
ajax.getJSON('https://elephant-api.herokuapp.com/elephants/random')
),
debug({
next: () => console.timeEnd('Measure Perf'),
})
)
.subscribe();
// OUTPUT
// Measure Perf 14.07653492ms
š„ļø Remote logging for increased observability
rxjs-debug-operator
has a Global Config that also allows you to change the logger that is used.
As such, you could potentially use something like Winston as your logger!
import { DebugLogger, setGlobalDebugConfig } from 'rxjs-debug-operator';
const winston = require('winston');
const sysLogger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
//
// - Write all logs with level `error` and below to `error.log`
// - Write all logs with level `info` and below to `combined.log`
//
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
const debugLogger: DebugLogger = {
log: (v) => sysLogger.info(v),
error: (e) => sysLogger.error(e),
};
setGlobalDebugConfig({ logger: debugLogger });
const obs$ = of('my test value');
obs$.pipe(debug()).subscribe();
// OUTPUT
// 'my test value' written to `combined.log`
š¤© Conclusion
This all started as a joke.
How many times do developers
tap(console.log)
with RxJS?
But it has flourished into so much more! Hopefully, this article shows what is possible with such a simple operator, and I look forward to seeing what weird and wonderful things developers start to do with it!
If you notice any problems or have a feature request, open an issue over on the Repo https://github.com/Coly010/rxjs-debug-operator!
If you have any questions, please ask below or reach out to me on Twitter: @FerryColum.
Posted on March 23, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.