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

Enhancing Vue functionality with vue-class-component and Mixins

In my Vue project, I am using vue-class-component along with TypeScript. Within the project, I have a component and a Mixin set up as follows: // MyComp.vue import Component, { mixins } from 'vue-class-component' import MyMixin from './mixi ...

Using a static value in the comparator is necessary for Array.find to function properly in Typescript

Looking to retrieve an item from an array: const device = this.selectedDevtype.devices.find(item => console.log(this.deviceID); return item.device_id === this.deviceID; }); console.log(device); When this.deviceID is logged, it shows "4", but t ...

One way to incorporate type annotations into your onChange and onClick functions in TypeScript when working with React is by specifying the expected

Recently, I created a component type Properties = { label: string, autoFocus: boolean, onClick: (e: React.ClickEvent<HTMLInputElement>) => void, onChange: (e: React.ChangeEvent<HTMLInputElement>) => void } const InputField = ({ h ...

Attempting to compile TypeScript by referencing ng2-bootstrap using Gulp within Visual Studio

I've been struggling with this issue for a few days now, and I'm really hoping someone can help me out. Currently, I am experimenting with Angular2 in an aspnet core project. The setup involves using a gulpfile.js to build .ts files and transfer ...

Getting the id of a single object in a MatTable

I'm currently working on an angular 8 application where I've implemented angular material with MatTableDatasource. My goal is to retrieve the id from the selected object in my list: 0: {id: "e38e3a37-eda5-4010-d656-08d81c0f3353", family ...

"Typescript with React and Material-UI Table - A seamless user experience with no errors

I have been working on incorporating the "material-table" library into my TypeScript and React project, but I am facing an issue where the page appears blank without any compiling errors. Environment configuration: npm: 6.11.3 nodejs: 10.17.0 typescript: ...

Applying the `lean` method to Mongoose queries that retrieve arrays in TypeScript

When working with two Mongoose queries, I made the decision to utilize the .lean() method on both of them. It appears that using .lean() on a query that returns a single document works well: let something:Something; SomethingDocument.findOne({_id:theId}) ...

TypeScript encountered an error: The get call is missing 0 type arguments

I encountered a typescript error stating "Expected 0 type arguments, but got 1" in the line where my get call is returning. Can you help me identify what is wrong with my get call in this code snippet? public get(params: SummaryParams): Observable&l ...

Error message thrown when attempting to access Navigator InjectionToken in tests: ReferenceError - Navigator is not defined

I have created an abstraction for the Navigator object: export const NAVIGATOR: InjectionToken<Navigator> = new InjectionToken<Navigator>( 'An abstraction over window.navigator object', { factory: () => inject(WINDOW).navig ...

Angular - Ensuring correct rendering of a subcomponent with input parameter on the first update

Here is a snippet of code showcasing a list of educations and a component: <cdk-virtual-scroll-viewport itemSize="5" class="list-scroll"> <app-education-item *ngFor="let education of loadedEducations" ...

Having issues with TypeScript while using Redux Toolkit along with Next Redux Wrapper?

I have been struggling to find a solution. I have asked multiple questions on different platforms but haven't received any helpful answers. Can someone please assist me? Your help is greatly needed and appreciated. Please take some time out of your bu ...

Guide on extracting just the key and its value from a Filter expression in a DynamoDB Query using Typescript

Presented here is a filter expression and Key Condition. The specific set of conditions are as follows: {"Age":{"eq":3},"Sex":{"eq":"MALE"}} const params: QueryCommandInput = { TableName: my_tab ...

Creating the upcoming application without @react-google-maps/api is simply not possible

After incorporating a map from the documentation into my component, everything seemed to be functioning correctly in the development version. However, when attempting to build the project, an error arose: Type error: 'GoogleMap' cannot be used as ...

Dynamic user input using an enumeration

I am looking to develop a flexible input component (in React) that can dynamically generate different types of inputs based on the enum type provided along with relevant inputProps. The idea is to switch between different input components based on the type ...

What is the process for utilizing a Typescript Unit Test to test Typescript code within Visual Studio?

Currently, I am facing an issue while writing a unit test in Typescript to check a Typescript class. The problem arises when the test is executed as it is unable to recognize the class. To provide some context, my setup includes Typescript (1.4) with Node ...

Having issues with Vue 3 Typescript integration in template section

This particular project has been developed using the create-vue tool and comes with built-in support for Typescript. Key versions include Vue: 3.3.4, Typescript: 5.0.4 Here is a snippet of the code to provide context: // ComponentA.vue <script setup l ...

Can webpack effectively operate in both the frontend and backend environments?

According to the information provided on their website, packaging is defined as: webpack serves as a module bundler with its main purpose being to bundle JavaScript files for usage in a browser. Additionally, it has the ability to transform, bundle, or ...

Obtain form data as an object in Vue when passed in as a slot

Currently, I am working on developing a wizard tool that allows users to create their own wizards in the following format: <wiz> <form> <page> <label /> <input /> </page> <page> <label /> ...

A guide on implementing JSON data projection with TypeScript

{ "ClaimType": ["The 'Claim Type' should have a minimum length of 4 characters. You have only entered 2 characters."], "ClaimValue": ["The 'Claim Value' should have a minimum length of 4 characters. You have only entered 1 chara ...

What could be causing my sinon test to time out instead of throwing an error?

Here is the code snippet being tested: function timeout(): Promise<NodeJS.Timeout> { return new Promise(resolve => setTimeout(resolve, 0)); } async function router(publish: Publish): Promise<void> => { await timeout(); publish(&ap ...