Utilizing Eithers to effectively manage errors as they propagate through the call chain

I'm delving into functional programming and exploring different ways to handle errors beyond the traditional try/catch method. One concept that has caught my attention is the Either monad in various programming languages. I've been experimenting with applying this concept to a simple app built using Express and fp-ts.

Imagine a scenario where we have the following request handling architecture (let's say it's for retrieving an entity from the database):

Express -> route handler -> controller -> repository -> data source -> database

This setup opens up multiple points where errors could potentially occur. Let's start by examining the data source. To allow flexibility in switching data sources in the future, I initially defined my data source interface as follows:

export type TodoDataSource = {
  findAll(): Promise<ReadonlyArray<TodoDataSourceDTO>>;
  findOne(id: string): Promise<TodoDataSourceDTO | null>;
  create(data: CreateTodoDTO): Promise<TodoDataSourceDTO>;
  update(id: string, data: UpdateTodoDTO): Promise<TodoDataSourceDTO>;
  remove(id: string): Promise<TodoDataSourceDTO>;
};

The repository factory function takes this data source as an argument during creation:

export function createTodoRepository(
  dataSource: TodoDataSource,
): TodoRepository {
  return {
    findAll: async () => await findAll(dataSource),
    findOne: async (id: string) => await findOne(dataSource, id),
    create: async (data: CreateTodoDTO) => await create(dataSource, data),
    update: async (id: string, data: UpdateTodoDTO) =>
      await update(dataSource, id, data),
    remove: async (id: string) => await remove(dataSource, id),
  };
}

Similarly, the repository implementation conforms to the TodoRepository interface:

export type TodoRepository = {
  findAll(): Promise<ReadonlyArray<Todo>>;
  findOne(id: string): Promise<Todo | null>;
  create(data: CreateTodoDTO): Promise<Todo>;
  update(id: string, data: UpdateTodoDTO): Promise<Todo>;
  remove(id: string): Promise<Todo>;
};

As I attempt to integrate the use of Eithers, I find that my interfaces become closely intertwined and verbose.

Initially, I update the data source to utilize Either as its return type:

import * as TaskEither from 'fp-ts/TaskEither';

export type DataSourceError = { type: "DATA_SOURCE_ERROR"; error?: unknown };

export type TodoDataSource = {
  findAll(): Promise<
    TaskEither.TaskEither<DataSourceError, ReadonlyArray<TodoDataSourceDTO>>
  >;
  findOne(
    id: string,
  ): Promise<TaskEither.TaskEither<DataSourceError, TodoDataSourceDTO | null>>;
  create(
    data: CreateTodoDTO,
  ): Promise<TaskEither.TaskEither<DataSourceError, TodoDataSourceDTO>>;
  update(
    id: string,
    data: UpdateTodoDTO,
  ): Promise<TaskEither.TaskEither<DataSourceError, TodoDataSourceDTO>>;
  remove(
    id: string,
  ): Promise<TaskEither.TaskEither<DataSourceError, TodoDataSourceDTO>>;
};

Modifying the repository interface to accommodate Either:

import type * as TaskEither from "fp-ts/TaskEither";
import { type DataSourceError } from "../../infra/data-sources/todo.data-source";
import { type ParseError } from "../../shared/parsers";

export type TodoNotFoundError = { type: "TODO_NOT_FOUND"; id: string };

export type TodoRepository = {
  findAll(): Promise<
    TaskEither.TaskEither<DataSourceError | ParseError, ReadonlyArray<Todo>>
  >;
  findOne(
    id: string,
  ): Promise<
    TaskEither.TaskEither<
      DataSourceError | ParseError | TodoNotFoundError,
      Todo
    >
  >;
  create(
    data: CreateTodoDTO,
  ): Promise<TaskEither.TaskEither<DataSourceError | ParseError, Todo>>;
  update(
    id: string,
    data: UpdateTodoDTO,
  ): Promise<TaskEither.TaskEither<DataSourceError | ParseError, Todo>>;
  remove(
    id: string,
  ): Promise<TaskEither.TaskEither<DataSourceError | ParseError, Todo>>;
};

The repository and data source are becoming interconnected through their error types, but the repository introduces additional error types like PARSE_ERROR and TODO_NOT_FOUND. While this may not be problematic at this stage, if the call chain becomes longer with multiple services and repositories, managing all these error types within the interfaces could get cumbersome.

Is there a better way to approach this? Or is this complexity expected?

One alternative could be combining both approaches, using try/catch for exceptions and delegating error handling to the controller. Utilizing Either earlier in the call stack to manage errors effectively.

Answer №1

Exploring a crucial element of managing errors in functional programming, particularly with the Either monad, you've uncovered an important topic. Although initially daunting due to the verbosity and tighter coupling in types, explicitly handling error types at each level is a common practice in FP.

Your valid concern about propagating errors up the call stack highlights the need for thoughtful error handling. It's best for the controller to focus on interpreting errors within the context of the request and crafting suitable responses rather than diving into low-level error specifics.

One potential enhancement could involve introducing a shared error union type or a more detailed error-handling approach. For example:

type AppError =
  | DataSourceError
  | ParseError
  | TodoNotFoundError
  // Include additional specific errors as needed

// Within the repository interface
export type TodoRepository = {
  findAll(): Promise<TaskEither.TaskEither<AppError, ReadonlyArray<Todo>>>;
  findOne(id: string): Promise<TaskEither.TaskEither<AppError, Todo>>;
  // ...
}

This consolidation may streamline the types and offer a centralized method for addressing errors. Moreover, as your application scales, it's essential to weigh the pros and cons of explicit error handling against the complexity it brings.

Employing try/catch alongside Either can be a viable strategy. Reserve try/catch for truly unexpected scenarios (such as catastrophic failures) while using Either for recoverable errors or expected failures within your domain.

Varying both approaches can strike a balance between FP's precise error management and conventional exception-based handling. Maintaining a clear distinction between them is crucial for coherence and uniformity in your codebase.

Remember, striking a harmonious balance between explicit error handling and practical code maintenance is crucial. Each project has unique demands and trade-offs, so adapting your error-handling strategy accordingly is sound practice.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Tips for creating a typescript typeguard function for function types

export const isFunction = (obj: unknown): obj is Function => obj instanceof Function; export const isString = (obj: unknown): obj is string => Object.prototype.toString.call(obj) === "[object String]"; I need to create an isFunction method ...

Is there a way to clear the selected date in a date picker while using MatDateRangeSelectionStrategy?

Recently, I was experimenting with the date picker feature of Angular Material and stumbled upon this particular example After implementing this example with my own logic, everything seemed to be working perfectly fine except for one issue. The dates were ...

"Trouble accessing the URL" error encountered when trying to load templateUrl for dynamic components in Angular 2

Attempted to modify a solution found here. The modification works well, but when changing the template to templateUrl in the component that needs to be loaded dynamically, an error occurs: "No ResourceLoader implementation has been provided. Can't rea ...

Deleting and inserting an element in the Document Object Model

I am currently working on developing a framework and need to create a directive called cp-if. Unlike the existing cp-show directive, where I can simply change the visibility of an element to 'none' and then make it visible again, with the cp-if d ...

The function webpack.validateSchema does not exist

Out of the blue, Webpack has thrown this error: Error: webpack.validateSchema is not defined Everything was running smoothly on Friday, but today it's not working. No new changes have been made to the master branch since Friday. Tried pruning NPM ...

Guidance on specifying a type based on an enum in Javascript

I have a list of animals in an enum that I want to use to declare specific types. For instance: enum Animals { CAT = 'cat', DOG = 'dog', } Based on this Animal enum, I wish to declare a type structure like so: type AnimalType = { ...

Promise rejection not handled: The play() function was unsuccessful as it requires the user to interact with the document beforehand

After upgrading my application from Angular 10 to 11, I encountered an error while running unit tests. The error causes the tests to terminate, but strangely, sometimes they run without any issues. Does anyone have suggestions on how to resolve this issue? ...

Set the value of HTML input type radio to a nested JSON string

Currently, I'm developing an Angular application and encountering an issue where I am unable to access the nested array value 'subOption.name' for the input type radio's value. I'm uncertain if the error lies within the metaData st ...

Leveraging interfaces with the logical OR operator

Imagine a scenario where we have a slider component with an Input that can accept either Products or Teasers. public productsWithTeasers: (Product | Teaser)[]; When attempting to iterate through this array, an error is thrown in VS Code. <div *ngFor= ...

The outcome of using Jest with seedrandom becomes uncertain if the source code undergoes changes, leading to test failures

Here is a small reproducible test case that I've put together: https://github.com/opyate/jest-seedrandom-testcase After experimenting with seedrandom, I noticed that it provides consistent randomness, which was validated by the test (running it multi ...

Is there a way to detect and handle errors triggered by a callback function?

My component has the following code snippet: this.loginService.login(this.user, () => { this.router.navigateByUrl('/'); }); Additionally, my service contains this method: login(credentials, callback) { co ...

Eliminate all citation markers in the final compiled result

Currently, I am consolidating all my .ts files into a single file using the following command: tsc -out app.js app.ts --removeComments This is based on the instructions provided in the npm documentation. However, even after compilation, all reference tag ...

What could be the reason for receiving an undefined user ID when trying to pass it through my URL?

Currently, I am in the process of constructing a profile page and aiming to display authenticated user data on it. The API call functions correctly with the user's ID, and manually entering the ID into the URL on the front end also works. However, wh ...

Exploring ways to destructure the useContext hook with a null default value in your Typescript code

Initially, I set up a context with a null value and now I am trying to access it in another component. However, when I destructure it to retrieve the variables from the context, I encounter a TypeScript error: Property 'users' does not exist on ...

Encountering a TypeScript issue with bracket notation in template literals

I am encountering an issue with my object named endpoints that contains various methods: const endpoints = { async getProfilePhoto(photoFile: File) { return await updateProfilePhotoTask.perform(photoFile); }, }; To access these methods, I am using ...

incorrect indexing in ordered list

I am facing an issue with the ngIf directive in Angular. My objective is to create a notification system that alerts users about any missing fields. Here's a stackblitz example showcasing the problem: https://stackblitz.com/edit/angular-behnqj To re ...

Oops! The OPENAI_API_KEY environment variable seems to be missing or empty. I'm scratching my head trying to figure out why it's not being recognized

Currently working on a project in next.js through replit and attempting to integrate OpenAI, but struggling with getting it to recognize my API key. The key is correctly added as a secret (similar to .env.local for those unfamiliar with replit), yet I keep ...

Perfroming unit testing on base class using Jasmine and Angular framework

I have a common base class that I include in every grid component. Currently, I have the specifications for the grid component, but I want to create separate specifications for the base class in its own spec file. The goal is to eliminate redundant code ...

How does the type of the original array influence the inferred types of the destructured array values?

let arr = [7, "hello", true]; let [a, ...bc] = arr; typeof bc : (string | number | boolean)[] why bc type is (string | number | boolean) expect: because bc = ["hello", true], so bc type should be (string | boolean)[] ...

What are some ways to troubleshoot the TypeScript React demonstration application in Chrome?

Debugging a TypeScript app in the Chrome debugger is a straightforward process. First, you need to configure the tsconfig.json file: "sourceMap": true, Next, install ts-node and set a breakpoint in your TypeScript code using "debugger;" ...