RxJS Observables That Can't Fail
Jesse Warden
Posted on August 14, 2024
A habit I made in JavaScript, and later TypeScript, was to have Promises never fail, and instead return a Result type. This ensured async code worked like sync code, and didn’t randomly break the application. If someone decided to use async/await syntax, they could forget a try/catch and be “ok”. I only use that syntax in unit tests where you’d want things to explode sooner.
I’ve attempted recently to apply the same style to RxJS, and sadly it’s not working out. The first problem is that has the same contract as AWS Lambda: You can either get a return value, or an Exception if something went wrong. This allows AWS to support just about any programming language because almost all return values, and almost all have ways of making exceptions, intentionally, but most not.
In RxJS’s case, that tends to form the contract as well, with strong types, making it look much like the original JavaScript Promise interface. While you don’t see code (at least I haven’t, but I’d wager others haven’t either) that uses the then/catch syntax, it is there, specifically in promise.then(handleSuccess, handleReject). Most people just plop 1 catch at the very end since exceptions aren’t easy, or useful to work with in JavaScript.
RxJS added some minor, but helpful additions such as catchError and throwError which allows you to do some helpful error mangling while you’re inside a stream. However, the core problem remains: TypeScript does not enforce handling of errors, so you can miss a catch/error. There are various ESLint and TypeScriptLint rules you can utilize as well as enforcing certain contracts, but ultimately, you just “have to remember”. This gets hard, whether in OOP or FP code bases, to remember the chain of observables may not have an error handler.
“But don’t they test for the unhappy path?” Many don’t test first, some don’t test at all. Many are hitting services that are “mostly up”, so they see the error as an API problem, not a UI code problem, or even a UX problem.
Now, a quick recap if you haven’t read my dated article. The tl;dr; is to make sure a Promise doesn’t fail is to return a resolved promise in the .catch.
e.g.
.catch( () => Promise.resolve('it failed') )
Combined with TypeScript, you can then ensure that a type of Promise<Result<string>>
actually always returns a Result that has a string in it, else an err. While it seems like “You’re making TypeScript look like Rust, bruh, why the boxes in boxes?”, the good news is, you never have to wrap async/await in a try/catch. Not that I encourage that syntax out of unit tests, BUT if someone does, it’s safe, and TypeScript helps ensure you handle the Ok or Err part of the Result.
It works in practice sort of like this using psuedo TypeScript:
legitUser = (name:string):Promise<Result<boolean>> =>
fetch(`someurl/api/${name}`)
.then( res => res.json() )
.then( isLegit => Ok(isLegit) )
.catch( e => Promise.resolve(Err(e?.message || 'failed' )) )
Then, you’d get 1 of these 3 scenarios:
result = await legitUser('Jesse')
// Ok(true)
// Ok(false)
// Err(Server don't know no Jesse)
So you’d think you could apply the Result style to RxJS, but… because RxJS’s types are MUCH better, and the convention around RxJS is to often either handle the next/error in the subscribe call (e.g. { next: someFunction, error: someErrorFunction }), OR which you see often in Angular is to just always subscribe(happyPath) as if nothing could ever go wrong.
So to play to that angle, could type some observable as:
Observable<Result<boolean>>
… and while TypeScript and RxJS play decently nice, http does not. When encountering server errors, HTTP will send back 2 types of errors as an error. The Angular docs from last year and years past encouraged to handle the error, as an error, and then re-throw it. The new Angular docs do not, but still assume you’ll use something like catchError to deal with it in an error context.
While catchError is promising because you could in theory map it back to a useable value, the “pattern”, “convention” or whatever you want to call it is “it’s an error, we must throw it because RxJS will ensure only 1 value is emitted, or 1 error, and this is how life is in RxJS”. Which … isn’t true; using catchError will allow to do the same thing; catch the error, and convert to a Result.Err(‘something went wrong’)
The Effect.ts people are all like “duh”, but the RxJS crowd is like “yeah… you’re starting to sound like the people who say you should just convert RxJS observables to promises in Angular.”
There is the possibility to let your types do the talking, like we showed above:
Observable<Result<boolean>>
but again, the idea of “Why do I get this result thing? An observable already tells me if something worked or failed via an error… why hide this Result thing in it?”
You really only have 1 response, and if this doesn’t resonate, I’d give up:
“The compiler can ensure you handle the Result.Ok, and Result.Err, but it won’t guarantee you’ve put a catchError in the pipe, and did NOT put a throwError after”.
I a final irony, this article ended on a bad result. 😅
Posted on August 14, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.