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)