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

What are the steps to ensure a successful deeplink integration on iOS with Ionic?

Recently, I was working on a hybrid mobile app for Android/iOS using Nuxt 3, TypeScript, and Ionic. The main purpose of the app is to serve as an online store. One important feature involves redirecting users to the epay Halyk website during the payment pr ...

PlayWright - Extracting the text from the <dd> element within a <div> container

Here is the structure I am working with: <div class="aClassName"> <dl> <dt>Employee Name</dt> <dd data-testid="employee1">Sam</dd> </dl> </div> I am attempting to retrie ...

There is no initial value set for the property and it is not definitively assigned in the constructor

I encountered an issue while working on the following code snippet: export class UserComponent implements OnInit { user: User; constructor() { } ngOnInit() { this.user = { firstName : "test", lastName ...

Tips for adding a border to a table cell without disrupting the overall structure of the table

Looking for a way to dynamically apply borders to cells in my ng table without messing up the alignment. I've tried adjusting cell width, but what I really want is to keep the cells' sizes intact while adding an inner border. Here's the sim ...

The functionality of the mat-radio-group is not rendered properly within an Angular formArray

Form elements that do not function properly with the formArray have been identified. Despite rendering, clicking on a button does not trigger any action. The editChoice() event fails to execute even when a break point is set in it. The controls also fail ...

The attribute 'sandwiches' cannot be found within the data type 'string'

In my app, I require an object that can store strings or an array of strings with a string key. This object will serve as a dynamic configuration and the keys will be defined by the user, so I cannot specify types based on key names. That's why I&apos ...

Is it possible to combine two separate host listeners into a single function in Angular 2?

One solution is to combine 2 different host listeners into a single function so that it can be called whenever needed. @HostListener('window:unload', ['$event']) unloadHandler() { this.eventService.send({ name: 'onUnload' }) ...

Issue encountered while attempting to utilize the useRef function on a webpage

Is it possible to use the useRef() and componentDidMount() in combination to automatically focus on an input field when a page loads? Below is the code snippet for the page: import React, { Component, useState, useEffect } from "react"; import st ...

Using ionic-v4 to browse through a application via its URL pathways

I am facing an issue with navigating to specific pages on my website using the URL. For example, I want to access page 1 by typing in the address bar: localhost:8100/mySite/page1 and go directly to page 1 localhost:8100/mySite/page3 and navigate to pag ...

A guide on leveraging Observable forkJoin with TransferObject in Ionic 3 and Angular 4

Using Express server and Multer for file handling. An array object named uploadArr holds the following data: var url = "http://192.168.8.101:3000/store_points_upload"; let targetPaths = []; let filenames = []; let optionLists = []; this.di ...

Encountering a type-safety problem while attempting to add data to a table with Drizzle

My database schema is structured like so: export const Organization = pgTable( "Organization", { id: text("id").primaryKey().notNull(), name: text("name").notNull(), createdAt: timestamp("c ...

Hold off on making any promises regarding Angular 2

Let me start by stating that I have gone through many responses and I am aware that blocking a thread while waiting for a response is not ideal. However, the issue I am facing is quite complex and not as straightforward to resolve. In my advanced project, ...

Unable to find a matching router for the Angular component

In my project, I am tasked with creating a specific path structure: "myapp/category/subcategory". The "myapp/" part is fixed, while the category is represented by the variable "cat.title" and subcategory by "sub.title". To achieve this, I have JSON data ...

Challenges Experienced by AoT in Live Operations

So in my project, I have various components and services included where necessary. To ensure their accessibility, I made sure to declare all the services as private within the constructor. Here's an example: constructor(private myService: MyService) ...

When I select a checkbox in Angular 2, the checkall function does not continue to mark the selected checkbox

How can I check if a checkbox is already marked when the selectAll method is applied, and then continue marking it instead of toggling it? selectAll() { for (let i = 0; i < this.suppliersCheckbox.length; i++) { if (this.suppliersCheckbox[i].type == " ...

Display notification if the request exceeds the expected duration

Is there a way to display a message when a request is taking too long? For instance, if the request exceeds 10 seconds in duration, I want to show a message asking the user to wait until it finishes. fetchData(url, requestParams) { return this.restServic ...

The process of removing and appending a child element using WebDriverIO

I am trying to use browser.execute in WebDriverIO to remove a child element from a parent element and then append it back later. However, I keep receiving the error message "stale element reference: stale element not found". It is puzzling because keepin ...

What is the best way to determine the amount of distinct elements in an array of objects based on a specific object property?

I am working with an array called orders. orders = [ {table_id: 3, food_id: 5}, {table_id: 4, food_id: 2}, {table_id: 1, food_id: 6}, {table_id: 3, food_id: 4}, {table_id: 4, food_id: 6}, ]; I am looking to create a function that can calculate ...

Sharing an object with a child component in Angular 2

After receiving data from a subscription, I am encountering an issue where my data is not binding to the local variable as expected. The scenario involves two components and a service. The parent component triggers a method in the service to perform an HT ...

"Looking to incorporate dynamic Carousel Indicators into your Angular2 project? Here's how you can

After moving to just one slide, the "next" and "prev" buttons stop working. Additionally, clicking on the indicators to move slides is also ineffective. Interestingly, when I remove the div with the class carousel-indicators, the "next" and "prev" buttons ...