dev-resources.site
for different kinds of informations.
RxJS Observables That Can't Fail
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. đ
Featured ones: