Stop Pretending The Error Never Happened! Be Explicit About Loading State

Rens Jaspers - Feb 27 - - Dev Community

We all need to stop using placeholder values that look like correctly resolved values in RxJS streams that error out!

This is a pattern you see in many tutorials:

items$ = this.http.get("api/items").pipe(
  catchError(() => of([]))
);
Enter fullscreen mode Exit fullscreen mode

It loads data asynchronously with RxJS, making an HTTP call to fetch a list of items from an API, and then uses the catchError operator to catch errors. But then instead of passing on an error value, it just returns a value as if it came from the API: of([]).

This approach is very dangerous and confusing for your users. For example, in your template, if you have:

@for(item of items$ | async; track item.id) {
  <div>{{ item.title }}</div>
} @empty {
  You have no items
}
Enter fullscreen mode Exit fullscreen mode

...it’ll show you have no items even if there was an error. It’s misleading! How is the user going to know what is going on? Do they really have zero items or is something wrong?

I get it: those tutorials probably want to skip proper error handling to keep videos or blog posts short, but it's better to omit catchError entirely than to teach incorrect practices. Errors should be displayed as errors!

So, how should it be done correctly?

Easy: just be explicit about returning error values. Be explicit about loading state. This can be as simple as mapping your resolved value to a loading state object such as (items) => ({ data: items, state: "loaded" }) and your errors to (error) => ({ error, state: "error" }).

The full type could be something like:

type LoadingState<T = unknown> =
  | { state: 'loading' }
  | { state: 'loaded'; data: T }
  | { state: 'error'; error: Error };
Enter fullscreen mode Exit fullscreen mode

In your template, you can then clearly show an error if there is one, avoiding misleading the user.

For example:

emails$ = this.emailService.getEmails().pipe(
  map(emails => ({ state: "loaded", data: emails })),
  catchError(error => of({ state: "error", error })),
  startWith({state: "loading"})
);
Enter fullscreen mode Exit fullscreen mode

Just like you should be explicit about error states, you should be explicit about load completion. I recommend always including a state property that explicitly indicates that the loading process is complete. This is another area poorly explained in tutorials: a loaded state is often implicitly equated with truthy values, which is not always accurate.

Avoid having to assume the loading state based on the raw value of your stream. Instead of guessing:

@if(emailCount$ | async; as count) {
  {{ count }} emails
} @else {
  Loading
}
Enter fullscreen mode Exit fullscreen mode

…which wrongly shows "Loading…" when the resolved email count equals a falsy 0, just be clear about the state by using explicit indicators.

Here’s a full example of how you could track loading and error states so you can accurately show your user what’s going on:

@Component({
  selector: 'app-root',
  template: `
    @if (emails$ | async; as emails) {
      @switch (emails.state) {
        @case ("loading") { Loading... }
        @case ("error") { Error: {{emails.error.message}} }
        @case ("loaded") {
          @for (let email of emails.data; track email.id); {
            <div>{{email.title}}</div>
          }
          @empty { You have no emails. }
        }
      }
    }
  `
})
export class AppComponent {
  private emailService = inject(EmailService);
  emails$ = this.emailService.getEmails().pipe(
    map(emails => ({data: emails, state: "loaded"})),
    catchError(error => of({error, state: "error"})),
    startWith({state: "loading"})
  );
}
Enter fullscreen mode Exit fullscreen mode

As you can see, being explicit about loading state and handling all the different outcomes does lead to a lot of boilerplate, and you should probably create some kind of reusable code for it.

This is exactly what I set out to do when I created *ngxLoadWith, a structural directive that lets you do all of the above in a declarative way.

Loading data will be as simple as this:


@Component({
  selector: 'app-root',
  
})
export class AppComponent {
  private emailService = inject(EmailService);
  emails$ = this.emailService.getEmails();
}
Enter fullscreen mode Exit fullscreen mode
<div *ngxLoadWith="emails$ as emails">
  @for (email of emails; track email.id) {
    <div>{{ email.title }}</div>
  } @empty { 
    You have no emails. 
  }
</div>
Enter fullscreen mode Exit fullscreen mode

If you want to show error and loading screens, just add references to ng-template:


<div *ngxLoadWith="emails$ as emails; loadingTemplate: loading; errorTemplate: error"  ">
  @for (email of emails; track email.id) {
    <div>{{ email.title }}</div>
  } @empty { 
    You have no emails. 
  }
</div>
<ng-template #loading>Loading...</ng-template>
<ng-template #error let-error>{{ error.message }}</ng-template>
Enter fullscreen mode Exit fullscreen mode

…at the small cost of adding a tiny 1.92 kb dependency to your Angular project.

Please check it out at https://github.com/rensjaspers/ngx-load-with and let me know what you think. If you find it useful, a star 🌟 is highly appreciated.

. . . . . . . . . . . . . .
Terabox Video Player