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.

You really only have 1 response, and if this doesn't resonate, I'd give up:or 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. ??

Stephen Hammond

Consultant + Mentor in Software Design and Architecture

6 个月

You know what would be nice... Promise wrapping a class (Success / Fail)

要查看或添加评论,请登录

Jesse Warden的更多文章

  • Error Handling for fetch in TypeScript

    Error Handling for fetch in TypeScript

    Originally posted at https://jessewarden.com/2025/02/error-handling-for-fetch-in-typescript.

    3 条评论
  • Encoding and Decoding in TypeScript

    Encoding and Decoding in TypeScript

    Originally posted at https://jessewarden.com/2025/02/encoders-and-decoders-in-typescript.

  • 1st Angular UI Story in 2025

    1st Angular UI Story in 2025

    After 11 months, I got my 1st UI story. Been doing Back-End-for-Front-End stories since we're a platform team.

    1 条评论
  • Elm to Angular for 10 Months

    Elm to Angular for 10 Months

    It's been 10 months since I went from Elm to TypeScript Angular, and I still struggle with the following: Void Return…

    1 条评论
  • Energy to Learn & Build When Burnt Out

    Energy to Learn & Build When Burnt Out

    Reminder to be nice to yourself right now if you're trying and failing to learn new things after work or practice;…

    11 条评论
  • YAGNI For Types

    YAGNI For Types

    Noticed a disturbing trend the past 3 years that I'll often end up with too many/overly verbose types. TDD has helped…

    4 条评论
  • Thoughts on ThoughtWorks Radar 2024

    Thoughts on ThoughtWorks Radar 2024

    The ThoughtWorks 2024 Radar was released (you can download the PDF with 1 click, no annoying sign up required). Below…

  • TypeScript's Lack of Naming Types and Type Conversion in Angular

    TypeScript's Lack of Naming Types and Type Conversion in Angular

    Random thoughts on TypeScript. I’ve noticed 2 things working on a larger Angular project: it is not common to name (aka…

    10 条评论
  • Six Alternatives to Using any in TypeScript

    Six Alternatives to Using any in TypeScript

    There are a few options, and strategies. We've listed from easiest to most thorough.

  • Dissecting Some Anti-TDD Statements

    Dissecting Some Anti-TDD Statements

    "You can't use TDD unless you know the requirements" Requirements Are Bogus The requirements are wrong. You're…

    3 条评论