DEV Community

Cover image for Errors and Angular
Armen Vardanyan for This is Angular

Posted on

Errors and Angular

Original cover photo by Brett Jordan on Unsplash.

No one likes Errors

As the title of this paragraph suggests, errors are generally something we try to avoid, and this is especially true for new developers.
But, as a matter of fact, errors are an essential part of the development process, and can even be our friends. So, we need to be careful about how we handle them. Also, it is always better to get a detailed error with a concise description of the problem, and maybe even an explanation of the solution.

Errors also happen in Angular, of course, and today we are going to examine how we can handle errors in Angular, how to differentiate between their types, and also when to throw our own, custom errors. So, let's get started!

Have a dedicated error handling service

It is always a good idea to have specific logic like logging, error handling and such to Angular services. Now such error handling service
has to be very flexible, and be able to handle different types of errors. For this example, we will put a difference between
HTTP request related errors and other system errors. Here is an example of such an error service:

import { Injectable } from "@angular/core";
import { HttpErrorResponse } from "@angular/common/http";

enum HttpErrorCodes {
  BadRequest = 400,
  Unauthorized = 401,
  Forbidden = 403,
  ServerError = 500,
}

@Injectable()
export class ErrorService {
  handleError(error: Error) {
    if (error instanceof HttpErrorResponse) {
      this.handleHttpError(error);
    } else {
      // other handling
      console.error("Error:", error);
    }
  }

  private handleHttpError(error: HttpErrorResponse) {
    switch (error.status) {
      case HttpErrorCodes.BadRequest:
        // handle bad request error
        break;
      case HttpErrorCodes.Unauthorized:
        // handle unauthorized error
        break;
      case HttpErrorCodes.Forbidden:
        // handle forbidden error
        break;
      case HttpErrorCodes.ServerError:
        // handle server error
        break;
      default:
        // handle other http error
        break;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

So this is a basic service that handles all sorts of errors, and makes a distinction between HTTP errors and other errors. How can this be improved? For example, if we use some third party error tracking/logging, we can do this in this service by checking our environment:

export class ErrorService {
  constructor(
    private readonly environment: Environment,
    private readonly logger: LoggerService
  ) {}

  handleError(error: Error) {
    if (error instanceof HttpErrorResponse) {
      this.handleHttpError(error);
    } else {
      // other handling
      console.error("Error:", error);
    }

    // now, let's check the environment, and,
    // when in production, send the error info to our logging tool:
    if (this.environment.production) {
      this.logger.logError(error);
    }
  }

  // other methods
}
Enter fullscreen mode Exit fullscreen mode

Now, with this service, we are ready to start our journey into error handling.

Throwing errors when we know something is wrong

Sometimes we might make HTTP requests which will return an OK result, but the response data itself might indicate that not everything went well. For example, if we try to get a user that does not exist, we might get a 404 response, or we might get a { "error": "User not found" } response. Now, if we use Observables for HTTP requests, expected operators might become useless when dealing with this:

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"],
})
export class AppComponent {
  user$ = this.userService.getUser().pipe(
    catchError((error) => {
      // this WILL NOT work!
      // handle the error
      return of(null);
    })
  );

  constructor(private readonly userService: UserService) {}
}
Enter fullscreen mode Exit fullscreen mode

Now, we might think we have handled the error, but in reality, if the API returns an 200 OK response,
but with an explanation why we cannot access this user's data, we will not have a 404 error, and the handler will not be called.
How can this be fixed for HTTP requests? Now, we can, of course, write something like this:

@Component({
  ...
})
export class AppComponent {
  user$ = this.userService.getUser().pipe(
    map(response => {
      if (response.success) {
        return response.data;
      } else {
        return throwError(error);
      }
    }),
  );

  constructor(private readonly userService: UserService) {
  }
}
Enter fullscreen mode Exit fullscreen mode

But keep in mind that we can have multiple calls with this same error-not-error logic, and we would need to copy-paste this logic everywhere.
Alternatively, we can write a custom RxJS operator to do this, but then we will have to import it everywhere only to do this specific thing.
None of this solutions are ideal, so here is a better one: Let's write an HttpInterceptor to check this and throw an error for us:

export class ResponseErrorInterceptor implements HttpInterceptor {
  intercept(
    req: HttpRequest<any>,
    next: HttpHandler,
  ): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      map((event: HttpEvent<any>) => {
        if (event instanceof HttpResponse) {
          if (
            event.body?.hasOwnProperty('success') &&
            !event.body.success
          ) {
            throw new HttpErrorResponse({
              status: 400,
              url: event.url,
              error: event.body.error ?? 'Unknown Error',
            });
          }
        }
        return event;
      }),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Now in this case we handle a success property on a response object, but we can perform any kind of logic, and also have multiple interceptors like this to handle different scenarios. Now we can use the catchError operator to handle this:

@Component({
  ...
})
export class AppComponent {
  user$ = this.userService.getUser().pipe(
    catchError(error => of(handleError(error))),
  );

  constructor(private readonly userService: UserService) {
  }
}
Enter fullscreen mode Exit fullscreen mode

Validating data

Sometimes the type system that TypeScript provides is not enough to safeguard some algorithms. This is particularly common with Angular pipes, when we need to provide a specific form of data, otherwise our logic is going to have problems. For instance, if we have a pipe that finds a user by id we need to make sure the data is not only a number, but also, for example, is an integer and a positive number.
Otherwise, we might get a result that does not reflect reality if we accidentally pass a number that cannot in any way be a number.
To safeguard ourselves and other developers that might use our pipe from this, we can check and throw an error:

class IdError extends RangeError {
    constructor(providedNumber: number) {
        super(
          `Provided number ${providedNumber} is not a valid id. 
           It must be a positive integer.`);
    }
}

@Pipe({
    name: "findUserById",
})
export class FindUserByIdPipe implements PipeTransform {
    transform(users: User[], id: number): User {
        if (!(Number.isInteger(id) && id > 0)) {
            throw new IdError(id);
        }
        const user = users.find(user => user.id === id);
        return user;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now this specific error warns the user of a problem with the provided data, and we won't be dealing with a surprising
or hard-to-catch bug.

Notice we extend from RangeError and not just Error. Sometimes it is useful to extend from more specific error types
for better structure. In our case, this specific error is in fact a RangeError, so we use that class to extend.

Warning of design nuances/issues

Another class of use cases with errors is when we want to communicate to other developers about some code design specific.
A situation like this can arise, for example, when we have a component that accepts one input or another, but when provided with both
can result in an unexpected behavior. In this case, it is a good practice to throw an error, and let the developer
who uses our component know what is wrong:

class ComponentInputCompatibilityError extends Error {
    constructor(...properties: string[]) {
        super(`The following properties cannot be provided
               simultaneously: ${properties.join(', ')}`);
    }
}

@Component({
  ...
})
export class MyComponent {
    @Input() property: string;
    @Input() otherProperty: number;

    ngOnChanges(changes: SimpleChanges) {
        if (this.property && this.otherProperty) {
            throw new ComponentInputCompatibilityError(
                'property',
                'otherProperty',
            );
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case, we are now safe that no one will confuse and end up with a strange bug.

Warning: This use case might be indicative of a design issue, so most of the time the best practice would be to
refactor the code to avoid this situation. But of course there are times when changing large chunks of code is not viable.
In these cases, providing a descriptive error is very important.

In Conclusion

Errors are a powerful tool to help us to understand the code we are writing, and to make sure we don't have any unexpected behavior.
As we have seen, there are different scenarios in Angular apps when we need to either handle or throw errors, so feel free to use this article as a reference any time you are dealing with risky code.

Top comments (1)

Collapse
 
andar profile image
Vitalii • Edited

Thank you for useful topic :)
Could you please clarify some thing: in section “ Throwing errors when we know something is wrong” in the very last picture where we use catchError operator. Inside it we going to return operator of() with function handleError(error). As I understand this function we have to get from ErrorService? But didn’t find the injecting of the service inside AppComponent. It’s just misspelling?