If one of the log functions throws an uncaught exception, what is affected? Some possibilities - all of which you'd probably want to avoid - are:
- Some of the other loggers wouldn't fire
- The code that triggered the log() call would see the exception.
- The entire app might shut down
Now, if log destinations are explicitly try/catch wrapped, this is not an issue. But we might forget to do so. An Event Bus could help here. The event bus style of a logger would be: rather than composing multiple functions into a single one, each log destination becomes a bus listener. Let's say we have a dedicated Bus for logging, here's how we can write a log() function that puts a message on the bus for listener destinations to pick up:
import { Bus } from '@rxfx/bus';
const logs = new Bus<LogData>();
function log(logData: LogData) {
logs.trigger(logData);
}
function listener1 (logData: LogData) {
console.log(logData)
}
function listener2 (logData: LogData) {
somethingThatCouldThrow(logData)
}
const dest1 = logBus.listen(
() => true, // invokes listener if this returns true
listener1
);
const dest2 = logBus.listen(
() => true,
listener2
);
If we call log()
, and one of our listeners has an uncaught exception, the bus behaves like a circuit breaker - the listener with the uncaught error will simply be shut down (unsubscribed) and handle no further events.
This limits, by default, the scope of failures, and makes an app more robust.
With this approach, no other listener will be prevented from logging. Even more important, the code that calls trigger will never see the exception! Subscribers are isolated from each other, and the publisher is isolated from subscriber errors. This allows the number of subscribers to grow, without adding additional risk to publishers that a newly added publisher breaks them.
const dest3 = logBus.listen(
() => true,
errorProneListener,
{ error(e) { /* An error-handling Observer, allowing the destination to keep handling despite its errors */ } }
);
The bus is built from RxJS - the return value of logBus.listen()
is a Subscription
. So you can stop listening with dest2.unsubscribe()
.
And the 3rd argument to logBus.listen is an Observer
that allows callbacks to run on lifecycle events like complete
, and next
.
As a bonus, if a logging destination returns a Promise for its completion - such as an Adobe Analytics event sent via HTTP Post - any of the RxJS concurrency operators (mergeMap
, switchMap
, concatMap
, and exhaustMap
) can be employed to adjust the concurrency. In other words - to queue up analytics events being sent requires only a change to:
const dest4 = logBus.listen(
() => true,
sendToAdobe,
{ error: console.error },
+ concatMap
)
Or simply:
- const dest4 = logBus.listen(
+ const dest4 = logBus.listenQueueing(
() => true,
sendToAdobe,
{ error: console.error }
)
Information about listeners being shut down is available on bus.errors
:
bus.errors.subscribe(console.error);
Note that if your subscriber to .errors
throws, the app may shut down ungracefully.