DEV Community

Rathish Shriyan
Rathish Shriyan

Posted on

Enhancing Domain-Driven Design with Functional Error Handling in TypeScript

Adding Functional Flavor to Domain-Driven Design with Either

Incorporating functional programming concepts into domain-driven design (DDD) can add clarity and expressiveness to our code. When implementing business logic methods, we often face multiple potential outcomes. Handling these outcomes in a readable, maintainable way becomes crucial as business requirements evolve. This is where the Either<T> class shines, giving us an elegant solution to capture success and error states in a way that aligns with functional programming principles.


User Story Example: Creating a Profile

Let’s consider the following user story:

User Story:

As a user, I need to be able to create my profile in the application. I will provide certain information from a recent billing statement, and the system should identify me and set up my profile accordingly. Wholesale and cash customers are not allowed to register, nor are users without permission to add an account.

This requires validating user information, which could lead to a variety of outcomes:

  • UserNotFound
  • AccountIsAlreadyAssociated
  • AccountIsWholeSaleError
  • CashCustomerError
  • NoPermissionToAddAccountError

Previous Approach: Using Standard Error Types

In previous implementations, I used a standard error type in the method signature:

public AddAccountBusinessFlow(data): Promise<bool | Error>
Enter fullscreen mode Exit fullscreen mode

This approach returned either a boolean on success or an error on failure. Calling code could then check against various errors:

if (error == UserNotFound) return response.status(404);
else if (error == AccountIsAlreadyAssociated || error == AccountIsWholeSaleError) return response.status(400);
Enter fullscreen mode Exit fullscreen mode

While functional, this quickly becomes unwieldy as conditions grow. A more readable and expressive method signature would improve maintainability, self-documenting the method's possible outcomes.


Functional Solution: The Either Type

Inspired by Rust’s Result monad and Either type, I explored using TypeScript’s Either to handle these multiple outcomes in a more expressive manner. By using an Either type, we can clarify the semantics of our method signatures and improve readability in a domain-driven context.

Here's how an Either implementation can make the method more expressive:

Either<
    | UserNotFound
    | AccountIsAlreadyAssociated
    | CashCustomerError
    | NoPermissionToAddAccountError,
      AddAccountSuccess>
Enter fullscreen mode Exit fullscreen mode

With this Either type, all possible error conditions are listed on the left, and the success state (AddAccountSuccess) is on the right. This pattern makes it clear that the function can either succeed with a valid outcome or return one of the specific error states. The method signature itself now serves as documentation, making it easier for anyone reading the code to understand potential outcomes.

Either Implementation in TypeScript

Here’s the Either implementation I used, inspired by an excellent article on expressive error handling in TypeScript here:

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);
};
Enter fullscreen mode Exit fullscreen mode

This Either type allows us to encapsulate both success and error states, enforcing at compile time that the caller must handle both cases.

Using Either in AddAccountBusinessFlow

With the Either pattern, our AddAccountBusinessFlow can now return specific outcomes in a functional style, improving clarity and reliability. Here’s how we can use it:

public AddAccountBusinessFlow(data): Promise<Either<
  | UserNotFound
  | AccountIsAlreadyAssociated
  | CashCustomerError
  | NoPermissionToAddAccountError,
    AddAccountSuccess>> {

  // Business logic here...

  if (!userExists) return left(new UserNotFound());
  if (accountIsWholesale) return left(new AccountIsWholeSaleError());
  if (!userHasPermission) return left(new NoPermissionToAddAccountError());

  return right(new AddAccountSuccess());
}
Enter fullscreen mode Exit fullscreen mode

Benefits of the Either Approach in Domain-Driven Design

  1. Semantic Clarity: By naming each error explicitly in the method signature, we create a more readable and self-documenting method. This is invaluable as the codebase grows.

  2. Compile-Time Safety: Using Either enforces that both success and error states are considered, reducing the risk of unhandled errors and making refactoring safer.

  3. Functional Flow: Either facilitates functional-style error handling, reducing branching code and making error handling more declarative.

  4. Enhanced Testability: Testing individual outcomes is straightforward. The function’s output type captures all possible paths, making it easy to verify each scenario.

Conclusion

The Either type brings the benefits of functional programming to TypeScript, making complex business logic more manageable and expressive. By applying this to domain-driven design, we get a cleaner, more readable codebase that is easier to maintain and test.

Top comments (0)