Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
780 lines (569 sloc) 24.6 KB
templateKey title date updated description tags category image published
article
Functional Error Handling with Express.js and DDD | Enterprise Node.js + TypeScript
2019-08-14T10:04:10-05:00
2019-08-30T10:04:10-05:00
How to expressively represent (database, validation and unexpected) errors as domain concepts using functional programming concepts and how to hook those errors up to our Express.js base controller.
Node.js
TypeScript
Functional programming
DDD
Express.js
Enterprise software
Enterprise Node + TypeScript
/img/blog/enterprise-node/enterprise-node.png
true

Imagine a world where you could open a severely rattled bottle of Pepsi with brazen confidence, not hesitating to consider the possibility that it might end up drenching you.

Imagine a world where nothing ever went wrong; where you only ever had to consider the happy path.

Let me know when we discover that world, because it's certainly not the one we currently live in. And it's something that's especially true with programming.


Errors need love too ❤️

In most programming projects, there's confusion as to how and where errors should be handled.

They account of a large portion of our application's possible states, and more often than not, it's one of the last things considered.

  • Do I throw an error and let the client figure out how to handle it?
  • Do I return null?

When we throw errors, we disrupt the flow of the program and make it trickier for someone to walk through the code, since exceptions share similarities with the sometimes criticized GOTO command.

And when we return null, we're breaking the design principle that "a method should return a single type". Not adhering to this can lead to misuse of our methods from clients.

We've half solved this problem in a previous article where we explored the use of a Result class to mitigate the jerky behaviour of throwing errors all over the place. And we're also able to determine if a Result is a success or a failure through Result.ok<T> and Result.fail<T>, but there's something missing...

Errors have a place in the domain as well, and they deserve to be modeled as to be modeled as domain concepts

Since errors account for so much of program behaviour, we can't pretend to ignore them. And if we're serious about object-modeling, when we express our errors as domain concepts, our domain model becomes a lot richer and says a lot more about the actual problem.

That leads to improved readability and less bugs.

Let me ask you, for something really important, which would you rather receive if something went wrong?

new Error('Something brok, sorry :p haha')

or

type Response = Either<
  // Failure results
  CreateUserError.UsernameTakenError | 
  CreateUserError.EmailInvalidError | 
  CreateUserError.AccountAlreadyExistsError |
  CreateUserError.InsecurePasswordError
  , 
  // Success
  Result<any>
>

Here's what you should expect to know by the end of this article:

  • Why expressing errors explicitly is important to domain modeling
  • How to expressively represent errors using types
  • How to and why to organize all errors by Use Cases
  • How to elegantly connect errors to Express.js API responses

How common approaches to handling errors fail to express domain concepts

We briefly mentioned this, but two very common (and fragile) approaches to handling errors I've seen are to either (1) Return null or (2) Log and throw.

(1) Return null

Consider we had a simple JavaScript function to Create a User:

function CreateUser (email, password) {
  const isEmailValid = validateEmail(email);
  const isPasswordValid = validatePassword(password);

  if (!isEmailValid) {
    console.log('Email invalid');
    return null;
  }

  if (!isPasswordValid) {
    console.log('Password invalid');
    return null;
  }

  return new User(email, password);
}

You've seen code like this before. You've also written code like this. I know I have.

The truth is, it's not great. Returning null is a little bit lazy because it requires the caller to be aware of the fact that a failure to produce a User will actually produce a null.

That provokes mistrust and clutters code with null-checks everywhere.

  const user = CreateUser(email, password);
  if (user !== null) {
    // success
  } else {
    // fail
  }

Not only that, but there are two separate failure states that could occur here:

  • The email could be invalid
  • The password could be invalid

Returning null fails to differentiate between these two errors. We've actually lost expressiveness of our domain model.

Consider if we wanted our API to return separate error messages for those errors?

That requirement might lead us to (2) Log and throw.

(2) Log and throw

Here's another common (non-optimal) approach to error handling.

function CreateUser (email, password) {
  const isEmailValid = validateEmail(email);
  const isPasswordValid = validatePassword(password);

  if (!isEmailValid) {
    console.log('Email invalid');
    return new Error('The email was invalid');
  }

  if (!isPasswordValid) {
    console.log('Password invalid');
    return new Error('The password was invalid');
  }

  return new User(email, password);
}

In this case, we do get back an error message from the Error, but manually throwing errors means having to surround lots of your own code in try-catch blocks.

try {
  const user = CreateUser(email, password);
} catch (err) {
  switch (err.message) {
    // Fragile code
    case 'The email was invalid':
      // handle
    case 'The password was invalid':
      // handle
  }
}

Typically, we should aim to reserve try-catch blocks for operations that interact with the outside world (ie: we're not the ones throwing errors), like:

  • Operations against a database
  • Operations utilizing an external service (API, etc)

While that's an issue, the larger problem at hand is still that we're not really expressing our possible error states effectively. Refactoring this to TypeScript and expressing errors with types is a start.

A better approach to handling errors

Tldr? View the code on GitHub right here.

We were able to get pretty far with the Result<T> class, but the biggest fallback is that while there is often one possible success state, there are several error states.

The Result<T> class doesn't allow us to express those several other error states.

Consider a CreateUser use case given a username, email and password. What are all of the possible things that could go right?

  • The user was successfully created

OK, and what about all of the things that could go wrong?

  • The email was invalid
  • A user already has this username
  • A user already exists with this email
  • The password doesn't meet certain security criteria

So from end to end (Express.js API call to the database), we want to either a 201 - Created if successful, some 40O-x error code if there was a client error, and a 500-x error if something broke on our end.

Let's set it up.

"Use Case" refresher: In a previous article (Better Software Design w/ Application Layer Use Cases), we discovered that Use Cases are the features of our apps, decoupled from controllers so that they can be tested without spinning up a webserver.

interface User {}

// UserRepository contract
interface IUserRepo {
  getUserByUsername(username: string): Promise<User>;
  getUserByEmail(email: string): Promise<User>;
}

// Input DTO
interface Request {
  username: string;
  email: string;
  password: string;
}

class CreateUserUseCase implements UseCase<Request, Promise<Result<any>>> {
  private userRepo: IUserRepo;

  constructor (userRepo: IUserRepo) {
    this.userRepo = userRepo;
  }

  public async execute (request: Request): Promise<Result<any>> {
    ... 
  }
}

At this point, we have a CreateUserUseCase which dependency injects an IUserRepo.

The Request dto requires a username, email, and password and the return type is going to be either a success or failure of Result<any>.

Laying out what we need to check for, we can see that there are several potential error states.

class CreateUserUseCase implements UseCase<Request, Promise<Result<any>>> {
  private userRepo: IUserRepo;

  constructor (userRepo: IUserRepo) {
    this.userRepo = userRepo;
  }

  public async execute (request: Request): Promise<Result<any>> {

    // Determine if the email is valid
      // If not, return invalid email error

    // Determine if the password is valid
      // if not, return invalid password error

    // Determine if the username is taken
      // If it is, return an error expressing that the username was taken

    // Determine if the user already registered with this email
      // If they did, return an account created error

    // Otherwise, return success

    return Result.ok<any>()
  }
}

Using Value Objects to encapsulate validation logic, we can get a Result<Email> and look inside of it to see if it was successfully created or not.

class CreateUserUseCase implements UseCase<Request, Promise<Result<any>>> {
  private userRepo: IUserRepo;

  constructor (userRepo: IUserRepo) {
    this.userRepo = userRepo;
  }

  public async execute (request: Request): Promise<Result<any>> {

    const { username, email, password } = request;

    const emailOrError: Result<Email> = Email.create(email);

    if (emailOrError.isFailure) {
      return Result.fail<any>(emailOrError.message)
    }

    const passwordOrError: Result<Password> = Password.create(password);

    if (passwordOrError.isFailure) {
      return Result.fail<void>(passwordOrError.message)
    }

    ...
  }
}

But still, we're back at square one, instead of returning null or throwing an error, we're returning a failed Result<T> for both errors, which is still not helping the calling code distinguish between them.

We need to create types for these error messages.

Here's how we can do that.

Expressing all errors for the CreateUser use case

Let's first standarize an what an error is:

DomainError class

interface DomainError {
  message: string;
  error?: any;
}

Contained error namespaces

Also, using TypeScript namespaces, we can represent all of the errors for the CreateUser use case with:

/**
 * @desc General application errors (few of these as possible)
 * @http 500
 */

export namespace AppError {
  export class UnexpectedError extends Result<DomainError> {
    public constructor (err: any) {
      super(false, {
        message: `An unexpected error occurred.`,
        error: err
      })
    }

    public static create (err: any): UnexpectedError {
      return new UnexpectedError(err);
    }
  }
}

/**
 * @desc CreateUser errors
 */

export namespace CreateUserError {

  export class UsernameTakenError extends Result<DomainError> {    
    public constructor (username: string) {
      super(false, {
        message: `The username "${username}" has already been taken.`
      })
    }

    public static create (username: string): UsernameTakenError {
      return new UsernameTakenError(username);
    }
  }

  export class EmailInvalidError extends Result<DomainError> {    
    public constructor (email: string) {
      super(false, {
        message: `The email "${email}" is invalid.`
      })
    }

    public static create (email: string): EmailInvalidError {
      return new EmailInvalidError(email);
    }
  }

  export class AccountAlreadyExistsError extends Result<DomainError> {    
    public constructor () {
      super(false, {
        message: `The account associated with this email already exists.`
      })
    }

    public static create (): AccountAlreadyExistsError {
      return new AccountAlreadyExistsError();
    }
  }

  export class InsecurePasswordError extends Result<DomainError> {    
    public constructor () {
      super(false, {
        message: `The password provided wasn't up to security standards.`
      })
    }

    public static create (): InsecurePasswordError {
      return new InsecurePasswordError();
    }
  }
}

This was possible by making a couple of changes to our old Result<T> class:

Updated Result<T> class

export class Result<T> {
  public isSuccess: boolean;
  public isFailure: boolean
  public error: T | string;
  private _value: T;

  public constructor (isSuccess: boolean, error?: T | string , value?: T) {
    if (isSuccess && error) {
      throw new Error("InvalidOperation: A result cannot be successful and contain an error");
    }
    
    if (!isSuccess && !error) {
      throw new Error("InvalidOperation: A failing result needs to contain an error message");
    }

    this.isSuccess = isSuccess;
    this.isFailure = !isSuccess;
    this.error = error;
    this._value = value;
    
    Object.freeze(this);
  }

  public getValue () : T {
    if (!this.isSuccess) {
      return this.error as T;
    } 

    return this._value;
  }

  public static ok<U> (value?: U) : Result<U> {
    return new Result<U>(true, null, value);
  }

  public static fail<U> (error: any): Result<U> {
    return new Result<U>(false, error);
  }
}

Now, we easily represent can go back to our use case change the signature of our response:

type Response = Result<CreateUserError.EmailInvalidError> 
  | Result<CreateUserError.AccountAlreadyExistsError>
  | Result<CreateUserError.InsecurePasswordError>
  | Result<CreateUserError.UsernameTakenError>
  | Result<AppError.UnexpectedError>
  | Result<any>;

That's better, but what if there was a better way for us to be able to group and separate the success results from the error results?

Introducing the Either class

Either is a union type which is either Left (for failure) or Right (for success).

Check out the code, via Bruno Vegreville.

Either monad

export type Either<L, A> = Left<L, A> | Right<L, A>;

export class Left<L, A> {
  readonly value: L;

  constructor(value: L) {
    this.value = value;
  }

  isLeft(): this is Left<L, A> {
    return true;
  }

  isRight(): this is Right<L, A> {
    return false;
  }
}

export class Right<L, A> {
  readonly value: A;

  constructor(value: A) {
    this.value = value;
  }

  isLeft(): this is Left<L, A> {
    return false;
  }

  isRight(): this is Right<L, A> {
    return true;
  }
}

export const left = <L, A>(l: L): Either<L, A> => {
  return new Left(l);
};

export const right = <L, A>(a: A): Either<L, A> => {
  return new Right<L, A>(a);
};

Equipped with this, we:

  • Get the same observable behaviour from the Result<T> class, but now we can also
  • ...segregate success responses from failure responses

Here's the updated CreateUserUseCase using the Either class, returning Lefts for errors and Right for success results.

type Response = Either<
  CreateUserError.UsernameTakenError | 
  CreateUserError.EmailInvalidError | 
  CreateUserError.AccountAlreadyExistsError |
  CreateUserError.InsecurePasswordError
  , 
  Result<any> // OK 
>

class CreateUserUseCase implements UseCase<Request, Promise<Response>> {
  private userRepo: IUserRepo;

  constructor (userRepo: IUserRepo) {
    this.userRepo = userRepo;
  }

  public async execute (request: Request): Promise<Response> {

    const { username, email, password } = request;

    // We've also updated our email Value Object classes 
    // to return the failure results
    const emailOrError: Either<CreateUserError.EmailInvalidError, 
      Result<Email>> = Email.create({ email })

    if (emailOrError.isLeft()) {
      return left(emailOrError.value);
    }

    // Same thing with the Password Value Object
    const passwordOrError: Either<CreateUserError.InsecurePasswordError, 
      Result<Password>> = Password.create({ password });

    if (passwordOrError.isLeft()) {
      return left(passwordOrError.value);
    }

    try {
      const [userByUsername, userByEmail] = await Promise.all([
        this.userRepo.getUserByUsername(username),
        this.userRepo.getUserByEmail(email),
      ])
  
      const usernameTaken = !!userByUsername === true;
      const accountCreated = !!userByEmail === true;

      // More errors we know about can be expressed this way
      if (usernameTaken) {
        return left(CreateUserError.UsernameTakenError.create(username));
      }
  
      if (accountCreated) {
        return left(CreateUserError.EmailInvalidError.create(email));
      }

      // Success resultt
      return right(Result.ok())
    } catch (err) {
      // Any uncaught error
      return left(AppError.UnexpectedError.create(err))
    }
  }
}

Notice that even the Email and Password Value Objects can express and segregate their errors and success states?

Here's the Email value object for example:

Email value object

export class Email extends ValueObject<EmailProps> {

  get value (): string {
    return this.props.email
  }

  private constructor (props: EmailProps) {
    super(props);
  }

  private static isEmailValid (email: string): boolean {
    // Naive validation
    if (email.indexOf('.com') === -1) {
      return false;
    } else {
      return true;
    }
  }

  public static create (
    props: EmailProps
  ): Either<CreateUserError.EmailInvalidError, Result<Email>> {

    if (this.isEmailValid(props.email)) {
      return right(Result.ok<Email>(new Email(props)));
    } else {
      return left(CreateUserError.EmailInvalidError.create(props.email));
    }
  }
}

Hooking it all up to an Express.js API controller

Last thing we want to figure out how to do is to get this working with our Base Controller.

Since we've already encapsulated all of the potential Express.js error and success response messages, our code will look a lot more expressive.

class CreateUserController extends BaseController {
  private useCase: CreateUserUseCase;

  constructor (useCase: CreateUserUseCase) {
    super();
    this.useCase = useCase;
  }

  async executeImpl (): Promise<any> {
    const { username, email, password } = this.req.body;

    try {
      const result = await this.useCase.execute({ username, email, password });

      // If the use case has a failure response of some sort

      if (result.isLeft()) {
        const error = result.value;

        // Switch on the response, map it to the correct HTTP error.

        switch (error.constructor) {
          case CreateUserError.UsernameTakenError:
            return this.conflict(error.getValue().message)
          case CreateUserError.EmailInvalidError:
            return this.clientError(error.getValue().message);
          case CreateUserError.AccountAlreadyExistsError:
            return this.conflict(error.getValue().message);
          case CreateUserError.InsecurePasswordError:
            return this.clientError(error.getValue().message);
          default:
            return this.fail(error.getValue().message);
        }
      } else {
        return this.ok(this.res);
      }
    } 
    
    catch (err) {
      return this.fail(err);
    }
  }
}

Because the result of the use case is of the Either type, it forces the caller to look into if the request was successful or not, and handle them accordingly.

By using a switch statement on the constructor, we can get direct access to the type of error before mapping it to the correct Express.js HTTP response code.

Composing the controller with the use case

In order to use this controller in our app, we need to create it and then export it so that it can be used up by the main Express.js app.

modules/users/useCases/createUser/index.ts
import { CreateUserUseCase } from './CreateUserUseCase'
import { CreateUserController } from './CreateUserController';
import { userRepo } from 'modules/users'; // repo instance exported from module

// Create the CreateUserUseCase by injecting a UserRepo instance
const createUserCase = new CreateUserUseCase(userRepo);

// Create the CreateUserController by injecting the CreateUserUseCase
const createUserController = new CreateUserController(createUserCase);

// Export both from this module
export {
  createUserCase,
  createUserController
}

Connecting a controller instance to a route handler

modules/users/infra/http/routes/index.ts
import * as express from 'express'

// Grab the instance of the CreateUserController from the useCases
import { createUserController } from '../../../useCases/createUser'

const userRouter = express.Router();

userRouter.post('/', 
  // Hook the controller instance up to a POST call
  (req, res) => createUserController.execute(req, res)
)

export { userRouter } // Export the router so the main app can use it

And then we can hook this up to the main Express.js app instance with:

app/index.ts
import * as express from 'express'
import { userRouter } from 'modules/users/infra/http/routes'

const app = express();

... // Other handlers

app.use('/users', userRouter);

app.listen(8000, () => console.log('Express app started!'))

In conclusion, our project structure could look a little something like this:

src
  └ modules
    └ users                          # 'Users' subdomain
      └ domain                       # Domain models (entities, value objects)
        └ Email.ts                   # Email (value object)
        └ Password.ts                # Password (value object)
        └ User.ts                    # User (aggregate / entity)             
      └ infra                        # Infrastructure layer concerns (webservers, caches, etc)
        └ http
          └ routes
            └ index.ts               # Export a user router
        └ repos                      
          └ UserRepo.ts              # Facade to the sequelize user models
      └ useCases                     # All of the application layer features
        └ createUser                 # Feature/Use Case #1 - Create a user 
          └ CreateUserUseCase.ts     # Use Case (also known as application service)
          └ CreateUserController.ts  # Create user controller
          └ CreateUserErrors.ts      # Any use case-specific errors
          └ index.ts                 # Export controller (required) and use case (optional) from this module

Domain-Driven Design for the win

This is exactly the type of thinking that we do when we're working on Domain-Driven projects. Errors are a part of the Domain layer in our clean/layered architecture and this is an excellent way to start representing them accordingly.


If you'd like to see the code in it's entirety, it's right here on GitHub.

Additional reading

You can’t perform that action at this time.