Managing errors when dealing with Observables that are being replayed and have a long lifespan

StackBlitz: https://stackblitz.com/edit/angular-ivy-s8mzka

I've developed an Angular template that utilizes the `async` pipe to subscribe to an Observable. This Observable is generated by:

  1. Subscription to an NgRx Store Selector (which provides selected user filters)
  2. Switch-mapping to an API request using the selected user filters
  3. Executing various operators such as `map`, `filter`, etc. after the `switchMap` operation
  4. The steps 2 and 3 may repeat multiple times

The API request within `switchMap` might fail due to numerous reasons, resulting in an error notification.

We aim to address this error by displaying an error alert. This error alert should also be displayed by subscribing to another Observable with the `async` pipe. The error-path Observable should directly stem from the success-path Observable without involving intermediate Subjects or any side effects.

Challenges encountered with proposed solutions:

  • `catchError` or `materialize` within `switchMap`: Wrapping the error in another object alters the data structure of the notification, making it difficult to use subsequent filters like `map`, `filter`, etc. Operators such as `debounceTime` or `delay` can't directly handle the error since it's treated as a `next` notification.
  • `retry`: Since the source Observable is a replaying Subject, this will trigger a new API call upon each resubscription, potentially leading to an infinite loop if the server consistently responds with errors. Moreover, forwarding the error notification using `retry` becomes challenging.
  • Dispatching actions, emitting a Subject, or setting `this.error` within a `tap`: Incorporating side effects for a simple scenario like this seems unnecessary and contradictory to functional design principles.

product.service.ts

getProducts$() {
  return this.store.select(selectProductFilters).pipe(
    switchMap(filters => this.http.get("/api/products?" + encodeFilters(filters))
    // ... map, filter, delay
  );
}

products.component.ts

products$: Observable<Product[]>
error$: Observable<string>

ngOnInit() {
  this.products$ = this.productService.getProducts$();
  
  this.error$ = this.products$.pipe(
    // what can we do here to receive errors as notifications
  );
}

products.component.html

<div *ngIf="(products$ | async) as products">{{products | json}}</div>
<div class="error" *ngIf="(error$ | async) as error">{{error}}</div>

Therefore, the question arises:

How can we establish two separate Observables for our template - one emitting `next` notifications and the other emitting `error` notifications while keeping the source Observable operational even after encountering errors?

EDIT

We seek a comprehensive solution to this issue - while the example above involves just one `switchMap`, the resolution should be applicable to any Observable pipeline scenario. For instance, envision the Service structured as follows:

product.service.ts

getProducts$() {
  return this.store.select(selectProductFilters).pipe(
    switchMap(filters => this.http.get("/api/products?" + encodeFilters(filters))
    // ... map, filter, delay
    switchMap(...)
    // ... map, filter, delay
    switchMap(...)
  );
}

The solution must effectively manage errors occurring in any of these `switchMap` statements and relay them to the template.

Answer №1

Below is the method I would take:

product.service.ts

getProducts$(): Observable<Product[] | { hasError: boolean }> {
  return this.store$.pipe(
    switchMap(
      filters => this.httpClient.get("/api/products?" + encodeFilters(filters)).pipe(
        catchError(err => of({ hasError: true, msg: 'an error' })),
      )
    ),
    share(),
  )
}

products.component.ts

ngOnInit() {
  const [products$, errors$] = partition(this.productService.getProducts$(), (v: any) => !v.hasError);

  this.products$ = products$.pipe(
    filter(products => products.length > 0),
    map(products => products.map(product => ({name: product.name.toUpperCase(), price: product.price + ",00 €"}))),
    delay(300)
  );

  this.products$ = merge(this.products$, errors$.pipe(mapTo(null)));
  this.error$ = merge(this.products$.pipe(mapTo(null)), errors$);
}

It is crucial to utilize share() in getProducts(). This creates a Subject instance between data consumers and producers to prevent multiple subscriptions to the same source within the ngOnInit function.

The partition function subscribes to the provided source twice, once for each part.

This code block:

this.products$ = merge(this.products$, errors$.pipe(mapTo(null)));
this.error$ = merge(this.products$.pipe(mapTo(null)), errors$);

ensures that only errors or products are displayed based on what is available at the time.


You might be concerned about having too many subscribers, but the Subject mentioned earlier typically has 5 subscribers due to how components subscribe to the streams.

<div *ngIf="(products$ | async) as products">{{products | json}}</div>

subscribes to

merge(this.products$, errors$.pipe(mapTo(null)));
, resulting in 2 subscribers.

Meanwhile,

<div class="error" *ngIf="(error$ | async) as error">{{error}}</div>

subscribes to

merge(this.products$.pipe(mapTo(null)), errors$).pipe(debounceTime(0));
, totaling 5 subscribers due to the initial subscriptions plus handling of errors stream.

StackBlitz.


Edit

Another alternative approach:

product.service.ts

getProducts$(): Observable<Product[] | { hasError: boolean }> {
  return this.store$.pipe(
    switchMap(filters => this.httpClient.get("/api/products?" + encodeFilters(filters))),
    filter(products => products.length > 0),
    map(products => products.map(product => ({name: product.name.toUpperCase(), price: product.price + ",00 €"}))),
    delay(300),

    tap({ next: () => console.log('next notif'), error: () => console.log('error notif') }),

    // catch and pass along error with `hasError` flag for interception by `error$` stream
    // use `throwError` here to allow `retryWhen` to re-subscribe only when new data is emitted
    catchError(err => concat(of({ hasError: true, msg: err }), throwError(err))),
    
    // `skip(1)` - skip error from which an error resulted
    retryWhen(errors => errors.pipe(switchMapTo(this.store$.pipe(skip(1))))),
    
    // needed because `partition` subscribes **twice** to the source
    share(),
  )
}

products.component.ts

ngOnInit() {
  const [products$, errors$] = partition(this.productService.getProducts$(), (n) => !(n as any).hasError);

  this.products$ = products$ as  Observable<Product[]>;;
  this.error$ = errors$;
}

StackBlitz.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The variable 'BlogPost' has already been declared within the block scope and cannot be redeclared

When working with Typescript and NextJS, I encountered the following Typescript errors in both my api.tsx and blogPost.tsx files: Error: Cannot redeclare block-scoped variable 'BlogPost'.ts(2451) api.tsx(3,7): 'BlogPost' was also dec ...

Setting up Electron to utilize TypeScript's baseUrl can be achieved by following a few simple steps

In TypeScript, there is a compiler option known as baseUrl that allows you to use non-relative paths, like: import Command from "util/Command" as opposed to: import Command from "../../../util/Command" While this works fine during compilation, TypeScri ...

How should I structure my MySQL tables for efficiently storing a user's preferences in a map format?

My current project involves developing a web application that provides administrators with the ability to manage user information and access within the system. While most user details like name, email, and workID are straightforward, I am facing difficulty ...

Tips for binding to a single input box within an ngFor loop

Can anyone lend a hand with some code? I'm working on a straightforward table using ngFor, but I'm facing an issue with input binding. The problem is that all the input fields generated by ngFor display the same value when typing. How can I preve ...

The use of `super` in Typescript is not returning the expected value

Trying to retrieve the name from the extended class is causing an error: Property 'name' does not exist on type 'Employee'. class Person { #name:string; getName(){ return this.#name; } constructor(name:string){ ...

Warning: NG2 RC5 has deprecated the use of HTTP_PROVIDERS

With the release of Angular2 version RC5, there have been some changes such as deprecating HTTP_PROVIDERS and introducing HttpModule. While my application code is functioning properly with this change, I am facing difficulties in updating my Jasmine tests. ...

When checking for a `null` value, the JSON property of Enum type does not respond in

Within my Angular application, I have a straightforward enum known as AlertType. One of the properties in an API response object is of this enum type. Here is an example: export class ScanAlertModel { public alertId: string; public alertType: Aler ...

When using TypeScript's array intersection type, properties are not accessible when using methods like Array.forEach or Array.some. However, they can be accessed within a for loop

It was challenging to search for this problem because I may not have the correct technical terms, but I hope my example can help clarify it. Background: I am using query data selectors in react-query to preprocess query results and add some properties tha ...

What steps can be taken to troubleshoot and resolve this specific TypeScript compilation error, as well as similar errors that may

I am struggling with this TypeScript code that contains comments and seems a bit messy: function getPlacesToStopExchange(): { our: { i: number; val: number; }[]; enemy: { i: number; val: number; }[]; //[party in 'our' | 'enemy' ]: ...

The Angular router is directed to an empty URL path instead of the specified URL

I have encountered a strange issue while developing my angular app which involves the router functionality. All routes were working perfectly fine until I discovered that after a successful login, instead of navigating to the '/admin' path as int ...

Measuring the height of an element within its parent component using Angular 4

Check out my demo here I've created a basic parent component along with a child component. Is there a way to retrieve the height of the parent div from within the child component? import { Component, Input, ElementRef, OnInit, ViewChild } from &apo ...

The authService is facing dependency resolution issues with the jwtService, causing a roadblock in the application's functionality

I'm puzzled by the error message I received: [Nest] 1276 - 25/04/2024 19:39:31 ERROR [ExceptionHandler] Nest can't resolve dependencies of the AuthService (?, JwtService). Please make sure that the argument UsersService at index [0] is availab ...

Navigating through multiple checkbox values in Angular 4/5

Is there a way to retrieve values from checkboxes other than just true or false? Take a look at my template below: <h4>Categories</h4> <div class="form-check" *ngFor="let cat of categories$|async"> <input class="form-check-input" ...

The 'ngModel' property cannot be bound to the 'input' element as it is not recognized. Error code: ngtsc(-998002)

When attempting to add [(ngModel)]="email" to my Angular app, I encountered the error message "can't bind to 'ngModel' since it isn't a known property of 'input'". Despite already including import { FormsModule } fro ...

The typescript MenuProvider for react-native-popup-menu offers a range of IntrinsicAttributes

Looking to implement drop-down options within a Flatlist component, I've utilized the React Native Popup Menu and declared MenuProvider as the entry point in App.tsx. Encountering the following error: Error: Type '{ children: Element[]; }' ...

Dynamically incorporate new methods into a class

Currently, I am in the process of implementing setters and getters for items that will be stored in session storage. These methods are being written within a service. However, upon attempting to call these functions in my component, I am encountering a tra ...

Exploring how Django utilizes sessions in conjunction with Angular's cookies while integrating with Django Rest

Looking for a comprehensive example demonstrating how Django Rest Framework can send a session key to Angular for storage as a cookie. I've been trying to figure this out for days... I have Django running on port 8000 and in Angular's ng serve, ...

When switching tabs, Ion-select should not reload the selected name

Whenever I switch tabs and then return to the previous tab in Ionic, the select field that was previously set becomes null, even though the page is still loading and the variable is populated. <ion-header color="primary"> <ion-navbar> &l ...

Spring Boot - The Ultimate Guide to Hosting Angular2 Compiled Files

Currently, I am using a Spring Boot restful server alongside an Angular2 front-end application. In an attempt to streamline the process, I have been trying to host the front-end files on Tomcat in order to avoid serving them separately. Unfortunately, desp ...

In what scenario would one require an 'is' predicate instead of utilizing the 'in' operator?

The TypeScript documentation highlights the use of TypeGuards for distinguishing between different types. Examples in the documentation showcase the is predicate and the in operator for this purpose. is predicate: function isFish(pet: Fish | Bird): pet ...