Error Handler Either

September 25, 2022

You probably already threw an exception and then forgot to handle it at the, crucial point, and all your code went down the drain. In this article, I will teach you how to handle errors in a better way for large-scale applications without worrying about handling them.

Error Handler Either

An Either is basically a container for a value that can be success or error. With an Either, we can apply transformations to the contained value without having to worry if it is an error or not until we reach a point in our code where we want to handle the error if it has occurred.

The Either can receive two values as mentioned above Left and Right, and in the case of Left, they would be errors and the Right a success value (Either = Error | Success). After understanding this, let's move on to the code and it is important to note that I will use TypeScript and that the folder names are irrelevant to you.

1export type Either<LeftType, RightType> = Left<LeftType, RightType> | Right<LeftType, RightType>;

If you are a TypeScript developer for some time, you have noticed the generics. If you don't know what it is, study it first to make it easier to understand. I will assume you already know what they are generics.

In the LeftType, I will place the type(s) of error that I expect, and in the RightType, the possible success cases. So let's continue and now create the Left and Right classes.

1export class Right<LeftType, RightType> {
2 value: RightType;
3 constructor(value: RightType) {
4 this.value = value;
5 }
6}
7
8export class Left<LeftType, RightType> {
9 value: LeftType;
10 constructor(value: LeftType) {
11 this.value = value;
12 }
13}

Create the two missing classes and in these, I have already passed the same types that they received in the Either type, and in the constructor of each one, they receive a value with the type of the value that the class represents.

Well, until here, this doesn't say anything to us, so let's create two methods in the two classes isLeft and isRight. We will use these methods to know if the return of a function is a case of error or success.

1export class Right<LeftType, RightType> {
2 value: RightType;
3 constructor(value: RightType) {
4 this.value = value;
5 }
6
7 isLeft(): this is Left<LeftType, RightType> {
8 return false;
9 }
10 isRight(): this is Right<LeftType, RightType> {
11 return true;
12 }
13}
14
15export class Left<LeftType, RightType> {
16 value: LeftType;
17 constructor(value: LeftType) {
18 this.value = value;
19 }
20
21 isLeft(): this is Left<LeftType, RightType> {
22 return true;
23 }
24
25 isRight(): this is Right<LeftType, RightType> {
26 return false;
27 }
28}

In our Right class, the isRight method returns true because if we are in the Right class, it means that it is a success case, and the isLeft method returns false because if there is a success case, it means that there was no error. I did the opposite in the Left class.

Probably the typing has left you confused, but this is to ensure that TypeScript understands the type of the value that the classes receive, "it is as if we were recovering the lost values".

Now, to be able to use this in our application, let's create two functions left and right. Note that the names are lowercase because we already have classes with these same names.

1export function left<LeftType, RightType>(valueLeft: LeftType): Either<LeftType, RightType> {
2 return new Left(valueLeft);
3}
4
5export function right<LeftType, RightType>(valueRight: RightType): Either<LeftType, RightType> {
6 return new Right(valueRight);
7}

And these, as soon as I call the left function, I pass the error and in the same function it returns the methods of the Left class, and if you remember, I returned true in the isLeft method, which means that when verifying isLeft, I will have the boolean value true and in isRight the opposite value in this case, false. The same goes for the right function. now let's test it.

Development of a simple application using Either.

To do this, create a very simple example of an API without a framework or library. Start by creating a usecases folder and in it a user creation file:

1import { Either, left, right } from "../shared/error-handler/either";
2
3type User = {
4 id: string;
5 name: string;
6 email: string;
7 created_at?: Date;
8 password: string;
9};
10type UserParam = {
11 name: string;
12 email: string;
13 password: string;
14};
15
16type ResponseType = Promise<Either<Error, Omit<User, "password">>>;
17
18export class CreateUserUseCase {
19 async execute({ email, name, password }: UserParam): ResponseType {
20 const repository: User[] = [];
21
22 if (!email || !password) {
23 const notExists = email ? "password" : "email";
24 return left(new Error(`missing params ${notExists}`));
25 }
26
27 if (!name) return left(new Error(`missing params name`));
28
29 const isExists = repository.find((user) => user?.email === email);
30
31 if (isExists) {
32 return left(new Error("user already exists"));
33 }
34
35 const passwordHash = password;
36 const buildUser = {
37 name,
38 email,
39 password: passwordHash,
40 id: String(repository.length),
41 };
42 repository.push(buildUser);
43
44 return right({
45 id: buildUser.id,
46 name: buildUser.name,
47 email: buildUser.email,
48 });
49 }
50}

In the return typing of the execute method, I pass it as a Promise and use the Either by passing the global Error interface in the LeftType part and the User type I created in the RightType part. We could make custom errors, which would be even better, but this is enough to test it.

Note that where I should throw an exception, I return the left function that creates an instance of our Left class (returns isLeft as true), and in the end, I return the right function that creates an instance of our Right class.

1import { CreateUserUseCase } from "../usecases/create-user-usecase";
2
3export class CreateUserController {
4 async handle(request: any): Promise<any> {
5 const { email, name, password } = request;
6
7 const createUserUseCase = new CreateUserUseCase();
8
9 const result = await createUserUseCase.execute({ email, name, password });
10 if (result.isLeft()) {
11 return result.value.message;
12 }
13
14 return result.value;
15 }
16}

In the controller, usually express typing is used, but I used the any type to make it easier, but after instantiating the CreateUserUseCase class, I execute the execute method passing the destructed parameters in the request.

To check for errors, remember that we return left or right, because in these same returns, we have the instances of the Left or Right classes with the isLeft and isRight methods that return a boolean. With that in mind, we can check if there was an error using the isLeft method. If it is true, it means that it entered the Left class. If it is false, we already know that it is a success case, and we will have the value, which is a variable depending on the value of isLeft and isRight, it can be error or user. Let's go to the service code...

1import { CreateUserController } from "./controllers/create-user-controller";
2
3const createUserController = new CreateUserController();
4
5(async () => {
6 const successOrFailed = await createUserController.handle({
7 // email: "johndoe@gmail.com",
8 name: "john doe",
9 password: "j1275gs#52",
10 });
11 console.log({ successOrFailed });
12})();

Notice that we did not handle the errors with try catch. We just execute the Promise, and for that, I used an IIFE (Immediately Invoked Function Expression), which is a function in JavaScript that is executed as soon as it is defined.

And in this case, I expect an error because in the application, the email is mandatory. To start, let's install ts-node to run in a TypeScript file.

1yazaldefilimone in www/learnspace/either-api-exemple
2npm i -g ts-node

Now we can test and see the result:

1yazaldefilimone in www/learnspace/either-api-exemple
2ts-node src/server.ts
3{ successOrFailed: 'missing params email' }

Now let's remove the comment on the email and test to see if it returns the user with id 0:

1import { CreateUserController } from "./controllers/create-user-controller";
2
3const createUserController = new CreateUserController();
4
5createUserController
6 .handle({
7 email: "johndoe@gmail.com",
8 name: "john doe",
9 password: "j1275gs#52",
10 })
11 .then((successOrFailed) => console.log({ successOrFailed }));

Let's go to the terminal and run it again:

1yazaldefilimone in www/learnspace/either-api-exemple
2ts-node src/server.ts
3{ successOrFailed: { id: '0', name: 'john doe', email: 'johndoe@gmail.com' } }