DEV Community

Jemal Ahmedov
Jemal Ahmedov

Posted on

Error handling without try/catch blocks

The try/catch statement wraps a code block within a wrapper and catches any exception thrown from that same block. When building large applications it might get a bit tedious when you have to wrap lots of parts of the code within try/catch.

Instead of getting sick of try/catch, there is another way of checking if the executable throws an error, using custom error instances.

Building custom error class

interface ICustomErrorProps extends Error {
  status?: number;
  data?: any;
}

class CustomError {
  constructor(props: ICustomErrorProps) {
    this.status = props.status;
    this.message = props.message;
    this.data = props.data;
  }

  message: ICustomErrorProps["message"];
  status?: ICustomErrorProps["status"];
  data?: ICustomErrorProps["data"];
}
Enter fullscreen mode Exit fullscreen mode

The code above is building a custom error class which expects, the usual properties that can be found in an error, e.g. status, message, data.

Building an Error Validator

By using custom class, it can be easily determined what type of response has been returned by checking the instance of the response. To illustrate how to do this, here is an ErrorValidator, which will determine the type of response.

type IResponse<T> = T | CustomError;

class ErrorValidator {
  constructor() {
    this.isError = this.isError.bind(this);
    this.isSuccess = this.isSuccess.bind(this);
  }

  public isError<T>(result: IResponse<T>): result is CustomError {
    return result instanceof CustomError;
  }

  public isSuccess<T>(result: IResponse<T>): result is T {
    return !this.isError(result);
  }
}
Enter fullscreen mode Exit fullscreen mode

The IResponse type defines what type the response can be - in this case either success T or error CustomError.

The ErrorValidator has two functions, isError and isSuccess. The isError function is checking if the instanceof the object is the CustomError that was defined above.

The TypeScript type predicate result is CustomError will automatically cast the result to CustomError if the returned condition is true.

ErrorValidator in action

One way to see this in action is to build an abstraction for an HTTP client. The HTTP client can extend the ErrorValidator class so the validation functions can be easily accessible by the client instance.

Here is an example of an HTTP client:

class HttpClient extends ErrorValidator {
  public async request<T>(
    url: string,
    options?: RequestInit
  ): Promise<IResponse<T>> {
    return fetch(url, options)
      .then((response) => response.json())
      .then((result: T) => result)
      .catch((error) => new CustomError(error));
  }
}
Enter fullscreen mode Exit fullscreen mode

The request function of the HttpClient is returning a promise of the IResponse type defined above. The catch of the fetch creates a new instance of the CustomError which later on can be validated.

Here is an example of how to consume the HttpClient:

interface IUserDetails {
  firstName: string;
  lastName: string;
  dob: Date;
}

async function init() {
  const httpClient = new HttpClient();

  const userDetails = await httpClient.request<IUserDetails>(
    "https://my-domain.com/user-details"
  );

  if (httpClient.isError(userDetails)) {
    console.log("An error occurred: ", userDetails.message);
    // Do something with the error
    return;
  }
  console.log("API Response data: ", userDetails);
  // Do something with the success
}
init();
Enter fullscreen mode Exit fullscreen mode

The main trick to bear in mind when using the custom error classes is the instanceof operator. As this is a pure JavaScript operator the same approach can be taken without TypeScript. The only difference will be that it won't apply static type checking.

Top comments (0)