Search code examples
angularrxjsrxjs-pipeable-operatorsrxjs-observables

Observable transformation to handle backend server error


Apologies for the newbie/dump question about RxJS, just started since I am learning Angular.

So I've created an asynchronous validator static method that calls an API service method checking the uniqueness of username in the database. The code snippets below works when the backend server is up and running. When it is not, it prints an error log message which I should be handling in the presentation component instead.

What am missing here in the transformation step to handle such generics use case to convert both Observable<SuccessResponse> and Observable<ErrorResponse> to Observable<ValidationErrors | null>.

Validator

static usernameUnique(service: ApiService): AsyncValidatorFn {
    return (control: FormControl): Observable<ValidationErrors | null> => {
        return service.isUsernameTaken(control.value as string)
            .pipe(
                map((response: SuccessResponse) => response.data ? { taken: true } : null)
            );
    };
}

ApiService

isUsernameTaken(username: string): Observable<SuccessResponse | ErrorResponse> {
    return this.httpCient.get('/backend/account/checkUnique', { params: { username: username } })
                .pipe(catchError(this.handleError));
}

handleError(error: HttpErrorResponse) : Observable<ErrorResponse> {
    // return specific payload of ErrorResponse based on error.status
}

Thanks in advance.


Solution

  • I am not exactly sure where you are running into an issue. If you're trying to resolve the map to ValidationErrors inside the validator, you will need to check the type. In this example I'm returning an error named "apiError" to differentiate from the "taken" error, but I'm not sure how you are choosing to handle this case in your app.

    I am assuming that ErrorResponse contains an error field, and that SuccessResponse contains a data field.

    static usernameUnique(service: ApiService): AsyncValidatorFn {
      return (control: FormControl): Observable<ValidationErrors | null> => {
        return this.isUsernameTaken(control.value as string)
          .pipe(
            map(response => {
              if ('error' in response) { // response is ErrorResponse
                return { apiError: response.error };
              }
              return response.data ? { taken: true } : null;
            })
          );
      };
    

    If you are constructing actual class instances using new ErrorResponse() and new SuccessResponse() you can use intanceof instead of checking fields by using the in operator. e.g. if (response instanceof ErrorResponse).

    Alternatively, you can bubble the error back up to the validator, and handle the response in another catchError. I prefer this way because there is no conditional logic on the response from the api.

    Validator

    static usernameUnique(service: ApiService): AsyncValidatorFn {
      return (control: FormControl): Observable<ValidationErrors | null> => {
        return this.isUsernameTaken(control.value as string)
          .pipe(
            map(response => response.data ? { taken: true } : null),
            catchError((err: ErrorResponse) => of({ apiError: err.error }))
          );
      };
    }
    

    ApiService

    isUsernameTaken(username: string): Observable<SuccessResponse> {
      return this.http.get('/backend/account/checkUnique', { params: { username } })
        .pipe(
          map(response => response as SuccessResponse), // maps the api response to SuccessResponse
          catchError(this.handleError) // maps the error to ErrorResponse
        );
    }
    
    handleError(error: HttpErrorResponse): Observable<never> {
      let errorResponse: ErrorResponse;
      // specific payload of ErrorResponse based on error.status
    
      return throwError(errorResponse);
    }