Functional Error Handling with Express.js and DDD | Enterprise Node.js + TypeScript

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 Left
s 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.
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
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:
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
Discussion
Liked this? Sing it loud and proud 👨🎤.
Stay in touch!
Enjoying so far? Join 15000+ Software Essentialists getting my posts delivered straight to your inbox each week. I won't spam ya. 🖖
View more in Enterprise Node + TypeScript
You may also enjoy...
A few more related articles




Want to be notified when new content comes out?
Join 15000+ other Software Essentialists learning how to master The Essentials of software design and architecture.
17 Comments
Commenting has been disabled for now. To ask questions and discuss this post, join the community.
Like comments? We got 'em now. Wahoooo~
Hi,khalil, thanks for you great articles. I think the value object response shoudn't be related to usecase like 'CreateUserCase'
I'm wandering how will this approach affect code readability on a larger project. I imagine errors coming in from so many places in a richer context, where you would have a much deeper call stack. I image the error union type reaching 20-30 error types or more quite often.
This would also force a result check after most function calls.
I want to say that it depends on a variety of factors, and that ending up in that scenario is probably also a hint at a larger problem, but allow me to explain how I used in on a project with about 250 different use cases.
Recall that a Use Case is either a Command or a Query. They either make changes to the system or retrieve data from the system.
These are also the features of my application.
I like to think of these features as vertical slices of the entire app because an app is nothing more than the combination of all the features/use cases it supports.
As my application grows, does each slice get larger? That is, when I add new features, do I need to go and update every slice with more error types?
Nah, I just add more slices :)
This is the Single Responsibility Principle applied at the component level.
So the scenario that you described, reaching 20-30 error types, I've never experienced that, and I think it would also be very unlikely to experience that, at least if you think in terms of commands + queries / features / slices as I described.
I can see something like what you described happening in very coupled code without well-defined boundaries of what the features / use cases are.
There is a level of pragmatism that should be applied here though. Taking this to the extreme is what pure functional programmers are doing, where every possible conceivable error has a type, and it's also where functional utilities like fp-ts will come in handy, but I personally sit somewhere in the middle.
For example, I set a few rules. We don't type errors related to infrastructure. Only application and domain layer stuff. So if anything goes wrong when I try to persist a domain object using Sequelize or TypeORM, I'll make sure the application service wraps it in a try catch and then just calls it a DatabaseError or a more generic AppError. I don't care to reveal to the client/end-user that it was a "FK not exists" error and I don't care to map the Sequelize error code into a more specific error.
With just about everything, you'll want to consider the additional complexity and if it's right for you though. Personally, I wouldn't use Functional Error Handling on smaller CRUD apps.
Hi Khalil! Thanks for your great work on this blog. I'm so happy to finally find some comprehensible content about these complex subjects.
When I was reading this article I got the feeling that the function of the `Left` and `Right` classes and `result.isFailure()`/`result.isSucces()` were overlapping. They're both ways of distinguishing failure and succesful results in a safe way. I'm a bit of a newbie to typescript and ddd, but I tried combining them by making the Either monad less abstract and more specific to errors. This way they could replace the old Result class completely. It would help me out a lot if you could take a quick look at it and let me know if it makes any sense.
https://gist.github.com/waspeer/049d7be66f96609aa863b074af846ba9
Thanks! :)
I feel like Wannes is onto something. I tried writing with Result and Either/Left/Right and found that often it felt like I had nested Result inside itself.
What are your thoughts of just using Either/Left/Right as the result class and changing the names to Response/Success/Failure? The `value` property can store either a success value or an error and isLeft or isRight would denote whether or not its a success or failure as well as provide the constructor type-checking you've demonstrated. I think this would reduce redundant code where you're nesting the error or success response on another object (Result) that is then stored on the value property.
I tested it, but I don't get type checking.
If I update my code and return a "new" left and forget to update the function return type I'm screwed.
Either monad is so confusing
how about specifying http response code ? like 201, 401 etc.
Thank you for this very informative post. After reading and implementing the technique on a pet project I wondered: What is the benefit of the Result class when using an Either monad?
For example if we have an Entity T, would it not be cleaner to return
instead of
Basically (correct me if I'm wrong) the Result class lets us distinguish between successful and erroneous states, while the Either class distinguishes between a binary choice of different wording (left/right). So a Right value should always be a successful state in the example above, right?
Hi there ! I like the approach but maybe i misunderstood something. I tried in a project but i don't see any example on your github where you return object or array of object. You only return response in white label. I tried to, for example, ListUsers, so my ListUsers use case return an array a users, but in my controller, the only thing i get is "return this.ok(res);" saying OK. When i pass on the result, i don't have my users because i should call getValue from the result, but because it's a response, i don't have access to that method. Should I cast it back to a result<User[]> ? I feel like something is wrong.
Thanks !
What is keeping someone from doing:
I think Amir hit on my only concern with this approach. I'm a fan of having specific error classes and using Result, but extending Result for those errors feels contradictory... unless it's like we're "successfully" creating an error, but that again feels forced.
I've been looking at FluentResults lately, especially because I need to not fail fast in some instances and collect all validation errors when I try to construct a domain object so I can return that feedback to the user. Basically, the client wants to be able to enter incomplete information on a large form spanning multiple pages, and only do large scale validation when moving to the next step in the workflow. The FluentResults approach keeps errors and results separated more, and at least for me eases the cognitive dissonance. Unfortunately, it's in C# so I'm having to translate the parts of it that matter to me over to TypeScript.
Have you heard of Purify or Purifree? They're FP libraries for TypeScript. Purifree is a fork of Purify and includes point-free FP constructs on top of Purify. They contain an `Either<TLeft,TRight>` class, as well as an `EitherAsync<TLeft,TRight>` class (which is a God-send when dealing with a `Promise<Either<TLeft, TRighT>>`), as well as many other monadic classes!
With the implementation of Either, Left and Right, I don’t see we’re using Result.fail anymore, isn’t? Just Result.ok when success or returning an exception with left. Should we delete the Result.fail static function?
Hey Khalil, really nice article, do you have a github where I can clone all of this and play around with it,
Awesome post! Either monad is really a game changer.
Could you tell me please - why do we need Result class?
It seems useless by using Either.