angulartypescriptangular-httpclientangular-signals

How to catch request errors with httpResource in Angular v19


I'm trying to use the new Angular httpResource. So I created a service as follows

@Injectable({ providedIn: 'root' })
export class MyService {
    resourceConfig = signal<Partial<ResourceConfig>>({})
    response = httpResource(() => ({
        url: this.resourceConfig().path,
        method: this.resourceConfig().method,
        body: this.resourceConfig.payload
        };
    })

    connect(payload: unknown) {
        this.resourceConfig.set({
            path: '/connect',
            method: 'POST',
            payload
        })
    }
}

This code works, the call to the connect endpoint is executed, but then I don't get anything back. For testing I've checked all the response signals in the component that calls connect:

export class MyConnectComponent {
   private service = inject(MyService)

   connectResponseFn = effect(() => {
       const status = this.service.response.status().  // -> 2
       const error = this.service.response.error().    // -> undefined
       const resp = this.service.response.value().     // -> undefined
       const code = this.service.response.statusCode() // -> undefined
  })


  ngOnInit() {
      this.service.connect({test: true})
  }
}

How can I get responses in this situation?


Solution

  • Solution:

    You should not execute the inner properties like path, method or payload since they are not signals.

    @Injectable({ providedIn: 'root' })
    export class MyService {
        resourceConfig = signal<Partial<ResourceConfig>>({})
        response = httpResource(() => ({
            url: this.resourceConfig().path,
            method: this.resourceConfig().method,
            body: this.resourceConfig().payload,
        })
    
        connect(payload: unknown) {
            this.resourceConfig.set({
                path: '/connect',
                method: 'POST',
                payload
            })
        }
    }
    

    Resource Error handling:

    You can handle errors, by directly checking the error message in the HTML and show the error.

    @let error = getError(response.error());
    @if(error) {
      {{error.message | json}}
    } @else if (response.isLoading()) {
      Loading...
    } @else {
      {{response.value() | json }}
    }
    

    The problem is error method is that it returns unknown but to access the error message, we have to type it to HttpErrorResponse, we use a function to type it along with @let operator.

    getError(error: any) {
      return error as HttpErrorResponse;
    }
    

    You can also use the status property of error, to show different message on each error code.

    @let error = getError(response.error());
    @if(error) {
      @switch(error.status) {
        @case (401) {
          Access Unauthorized
        }@case (404) {
          API Not Found
        } @default {
          {{error.status}} - {{error.message}}
        }
      }
    }
    

    Below is a working Stackblitz highlighting the error handling mechanism:

    Full Code:

    import { Component, effect, inject, signal, Injectable } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import {
      HttpErrorResponse,
      httpResource,
      HttpResourceRequest,
      provideHttpClient,
    } from '@angular/common/http';
    import { JsonPipe } from '@angular/common';
    export interface ResourceConfig {
      path?: string;
      method?: string;
      payload?: any;
    }
    @Injectable({ providedIn: 'root' })
    export class MyService {
      resourceConfig = signal<ResourceConfig>({});
      response = httpResource(
        () =>
          ({
            url: this.resourceConfig()!.path,
            method: this.resourceConfig()!.method,
            body: this.resourceConfig()!.payload,
          } as HttpResourceRequest)
      );
    
      connect(payload: unknown) {
        this.resourceConfig.set({
          path: '/connect',
          method: 'POST',
          payload,
        });
      }
    }
    
    @Component({
      selector: 'app-root',
      imports: [JsonPipe],
      template: `
        @let error = getError(response.error());
        @if(error) {
          @switch(error.status) {
            @case (401) {
              Access Unauthorized
            }@case (404) {
              API Not Found
            } @default {
              {{error.status}} - {{error.message}}
            }
          }
        } @else if (response.isLoading()) {
          Loading...
        } @else {
          {{response.value() | json }}
        }
        `,
    })
    export class App {
      private service = inject(MyService);
      get response() {
        return this.service.response;
      }
      connectResponseFn = effect(() => {
        const status = this.service.response.status(); // -> 2
        const error = this.service.response.error(); // -> undefined
        const resp = this.service.response.value(); // -> undefined
        const code = this.service.response.statusCode(); // -> undefined
      });
    
      ngOnInit() {
        this.service.connect({ test: true });
      }
    
      getError(error: any) {
        return error as HttpErrorResponse;
      }
    }
    
    bootstrapApplication(App, {
      providers: [provideHttpClient()],
    });
    

    Stackblitz Demo