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"];
}
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);
}
}
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));
}
}
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();
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)