Skip to content

Going Reactive with RxJS

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

RxJS is the perfect tool for implementing reactive programming paradigms to your software development. In general, software development handling errors gracefully is a fundamental piece of ensuring the integrity of the application as well as ensuring the best possible user experience.

In this article, we will look at how we handle errors with RxJS and then look at how we can use RxJS to build a simple yet performant application.

Handling Errors

Our general approach to errors usually consists of us exclaiming "Oh no! What went wrong?" but it's something that is a common occurrence in all applications. The ability to manage errors well without disrupting the user's experience, while also providing accurate error logs to allow a full diagnosis of the cause of the error, is more important than ever.

RxJS gives us the tools to do this job very well! Let's take a look at some basic error handling approaches with RxJS.

Basic Error Handling

The most basic way of detecting and reacting to an error that has occurred in an Observable stream provided to us by the .subscribe() method.

obs$.subscribe(
    value => console.log("Received Value: ", value),
    error => console.error("Received Error: ", error)
)

Here we can set up two different pieces of logic—one to handle non-error emission from the Observable and one to gracefully handle errors emitted by the Observable.

We could use this to show a Notification Toast or Alert to inform the user that an error has occurred:

obs$.subscribe(
    value => console.log("Received Value: ", value),
    error => showErrorAlert(error)
)

This can help us minimize disruption for the user, giving them instant feedback that something actually hasn't worked as appropriate rather than leaving them to guess.

Composing Useful Errors

Sometimes, however, we may have situations wherein we want to throw an error ourselves. For example, some data we received isn't quite correct, or maybe some validation checks failed.

RxJS provides us with an operator that allows us to do just that. Let's take an example where we are receiving values from an API, but we encounter missing data that will cause other aspects of the app not to function correctly.

obs$
  .pipe(
    mergeMap((value) =>
      !value.id ? throwError("Data does not have an ID") : of(value)
    )
  )
  .subscribe(
    (value) => console.log(value),
    (error) => console.error("error", error)
  );

If we receive a value from the Observable that doesn't contain an ID, we throw an error that we can handle gracefully.

NOTE: Using the throwError will stop any further Observable emissions from being received.

Advanced Error Handling

We've learned that we can handle errors reactively to prevent too much disruption for the user.

But what if we want to do multiple things when we receive an error or even do a retry?

RxJS makes it super simple for us to retry errored Observables with their retry() operator.

Therefore, to create an even cleaner error handling setup in RxJS, we can set up an error management solution that will receive any errors from the Observable, retry them in the hopes of a successful emission, and, failing that, handle the error gracefully.

obs$
  .pipe(
    mergeMap((value) =>
      !value.id ? throwError("Data does not have an ID") : of(value)
    ),
    retry(2),
    catchError((error) => {
        // Handle error gracefully here
      console.error("Error: ", error);
      return EMPTY;
    })
  )
  .subscribe(
    (value) => console.log(value),
    () => console.log("completed")
  );

Once we reach an error, emitting the EMPTY observable will complete the Observable. The output of an error emission above is:

Error:  Data does not have an ID
completed 

Usage in Frontend Development

RxJS can be used anywhere running JavaScript; however, I'd suggest that it's most predominately used in Angular codebases. Using RxJS correctly with Angular can massively increase the performance of your application, and also help you to maintain the Container-Presentational Component Pattern.

Let's see a super simple Todo app in Angular to see how we can use RxJS effectively.

Basic Todo App

We will have two components in this app: the AppComponent and the ToDoComponent. Let's take a look at the ToDoComponent first:

import {
  ChangeDetectionStrategy,
  Component,
  EventEmitter,
  Input,
  Output
} from "@angular/core";

export interface Todo {
  id: number;
  title: string;
}

@Component({
  selector: "todo",
  template: `
    <li>
      {{ item.title }} - <button (click)="delete.emit(item.id)">Delete</button>
    </li>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ToDoComponent {
  @Input() item: Todo;
  @Output() delete = new EventEmitter<number>();
}

Pretty simple, right? It takes an item input and outputs an event when the delete button is clicked. It performs no real logic itself other than rendering the correct HTML.

One thing to note is changeDetection: ChangeDetectionStrategy.OnPush. This tells the Angular Change Detection System that it should only attempt to re-render this component when the Input has changed.

Doing this can increase performance massively in Angular applications and should always be applicable to pure presentational components, as they should only be rendering data.

Now, let's take a look at the AppComponent.

import { Component } from "@angular/core";
import { BehaviorSubject } from "rxjs";
import { Todo } from "./todo.component";

@Component({
  selector: "my-app",
  template: `
    <div>
      <h1>
        ToDo List
      </h1>
      <div style="width: 50%;">
        <ul>
          <todo
            *ngFor="let item of (items$ | async); trackBy: trackById"
            [item]="item"
            (delete)="deleteItem($event)"
          >
          </todo>
        </ul>
        <input #todoTitle placeholder="Add item" /><br />
        <button (click)="addItem(todoTitle.value, todoTitle)">Add</button>
      </div>
    </div>
  `,
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  private items: Todo[] = [{ id: 1, title: "Learn RxJS" }];
  items$ = new BehaviorSubject<Todo[]>(this.items);

  addItem(title: string, inputEl: HTMLInputElement) {
    const item = {
      id: this.items[this.items.length - 1].id + 1,
      title,
      completed: false
    };
    this.items = [...this.items, item];
    this.items$.next(this.items);

    inputEl.value = "";
  }

  deleteItem(idToRemove: number) {
    this.items = this.items.filter(({ id }) => id !== idToRemove);
    this.items$.next(this.items);
  }

  trackById(index: number, item: Todo) {
    return item.id;
  }
}

This is a container component, and it's called this because it handles the logic relating to updating component state as well as handles or dispatches side effects.

Let's take a look at some areas of interest:

private items: Todo[] = [{ id: 1, title: "Learn RxJS" }];
items$ = new BehaviorSubject<Todo[]>(this.items);

We create a basic local store to store our ToDo items; however, this could be done via a state management system or an API.
We then set up our Observable, which will stream the value of our ToDo list to anyone who subscribes to it.

You may now look over the code and begin to wonder where we have subscribed to items$.

Angular provides a very convenient Pipe that handles this for us. We can see this in the template:

*ngFor="let item of (items$ | async); trackBy: trackById"

In particular, it's the (items$ | async) this will take the latest value emitted from the Observable and provide it to the template. It does much more than this though. It also will manage the subscription for us, meaning when we destroy this container component, it will unsubscribe automatically for us, preventing unexpected outcomes.

Using a pure pipe in Angular also has another performance benefit. It will only ever re-run the code in the Pipe if the input to the pipe changes. In our case, that would mean that item$ would need to change to a whole new Observable for the code in the async pipe to be executed again. We never have to change item$ as our values are then streamed through the Observable.

Conclusion

Hopefully, you have learned both about how to handle errors effectively as well put RxJS into practice into a real-world app that improves the overall performance of your application. You should also start to see the power that using RxJS effectively can bring!

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co