Functional error handling in Javascript with Either

Functional error handling in Javascript with Either

The topic of error handling in Javascript occurs to almost everyone. I should note that quite a lot of articles have already been written on this topic. That's why I won't go into details and basics. I will offer my own variant of error handling.

First, let me remind you that an error in Javascript is essentially “throwing an exception”. The exception needs to be handled by the application. By default, an exception is thrown by the Error object.

The traditional try/catch approach has its disadvantages: lack of strict typing in the catch block, confusion between technical failures and business logic errors, and frequent cases when developers forget to handle errors in critical places. This problem can be solved by using the Either pattern, which allows you to explicitly separate successful and erroneous results. In this article, we will describe how to implement error handling in TypeScript by creating custom exceptions and show how a functional approach to handling errors can be used using the Monet library.

1. Error handling in the standard way

The usual way of handling errors is to use try/catch blocks. However, the catch block lacks error typing (the default type is any), which may lead to difficulties in further analysis of the exception. Besides, it is difficult to separate technical errors (for example, database connection errors) from business logic errors.

To solve this problem, custom errors are often written. Here is an example of creating a custom error:

 

class OrderError extends Error {
  constructor(code: string, message: string) {
    super(message);
    this.name = "OrderError";
    this.code = code; // insufficient_funds, invalid_payment, incorrect_id etc
    const codeMessageMap = {
        'insufficient_funds': 'You do not have enough funds to complete this order',
        'invalid_payment': 'There was an error during last payment',
        'incorrect_id': 'Wrong ID was provided',
    }
    this.userMessage = codeMessageMap[code] || "An error occurred with your order"
  }
}

function processOrder(orderId: number): void {
  if (orderId <= 0) {
    throw new OrderError("incorrect_id", "Wrong ID");
  }
  
}

 

This approach allows you to define the error type more explicitly, but the catch block itself continues to work with the any type, which is not always convenient.

Here is how error handling will look like in this case:

 

try{
    processOrder(2)
}catch(err){
    if(err instanceof OrderError){
        if(err.code === 'incorrect_id'){
            alert(err.userMessage)
            console.log(err.message);
        }
        if(err.code === 'invalid_payment'){
            // show message to user
        }
    } else{
        alert('Something went wrong! Please contact support')
        console.log(err.message);
    }
    
}

 

Javascript errors also support nesting. In the example below, we catch an error, wrap it in our own, preserving the context and adding a clearer description.

 

function displayMoney(cents){
    try{
        document.querySelector("#money").innerText = formatMoney(cents)    
    }catch(err){
        if(err.message.includes('innerText')){
            throw new Error('Div with the id of money is missing from the page', {
                cause: err
            })
        }
        throw err;
    }
}

 

It is often difficult to distinguish technical errors from business logic errors. Consider the following example, where we throw an error after processing an API request, thus at the level above we can't accurately understand whether the problem is technical or business logic.

 

async function fetchWithRetry(url, attempts = 3){
    while(attempts > 0){
        try{
            const res = await fetch(url)
            if(res.ok){
                return res
            }else{
                throw new Error("Network response was not ok")
            }
        }catch(err){
            attempts--;
            if(attempts === 0){
                throw err
            }
        }
    }
}

try{
    await fetchWithRetry("https://httpbin.org/status/200,500")
}catch(err){
    console.log("caught retry example", err)
}

console.log("end!")

 

2. Either principle and advantages of functional error handling

The Either pattern is widely used in functional programming. Its essence is that a function returns not an exception but a result, which can be either successful (Right) or erroneous (Left). This approach is similar to error handling in Go, where errors are returned along with the result rather than being thrown away.

Advantages of Either:

  • Clear separation between a successful outcome and an error.
  • Ability to compose multiple functions without the need for multiple try/catch blocks.
  • Transparent error handling - the result explicitly reflects whether the operation is successful or not.

3. Example of error handling implementation using Either

In this example, we implement the function of updating an email client in CRM. To work with Either, we use the Monet library. If during the update it is detected that the client does not exist or an incorrect email is passed, the function returns Either.left with the corresponding error. Otherwise - Either.right with updated client data.

Code example:

 

import { Either } from "monet";

interface Client {
  id: number;
  name: string;
  email: string;
}

class NotFoundError extends Error {
  constructor(code: int, message: string) {
    super(message);
    this.name = "NotFoundError";
    this.code = code
  }
}

class ValidationError extends Error {
  constructor(field: string, message: string) {
    super(message);
    this.name = "BusinessError";
    this.field = field;
  }
}

const fakeDatabase: Client[] = [
  { id: 1, name: "Иван Иванов", email: "ivan@example.com" },
  { id: 2, name: "Мария Петрова", email: "maria@example.com" }
];

function updateClientEmail(clientId: number, newEmail: string): Either<Error, Client> {
  const client: Client | undefined = fakeDatabase.find(client => client.id === clientId);
  
  if (!client) {
    return Either.Left(new NotFoundError(404, `Клиент с id=${clientId} не найден`));
  }
  
  if (!newEmail.includes("@")) {
    return Either.Left(new BusinessError("email", "Wrong email provided"));
  }
  
  client.email = newEmail;

  return Either.Right(client);
}

// How to use it
const result = updateClientEmail(1, "newemail@example.com");

result.cata(
  (error) => console.error("Ошибка при обновлении клиента:", error.message),
  (client) => console.log("Клиент успешно обновлен:", client)
);

 

In this example:

  • The updateClientEmail function returns Either.
  • If an error occurs (for example, the client is not found or the email is incorrect), it returns Either.Left with the error object.
  • If everything was successful, Either.Right with the updated client object is returned.
  • The cata method allows you to “unwrap” Either and execute one of the functions, depending on a successful or erroneous result.

4. Explanation and opportunities for further improvement

How Either works:

  • The left option (Left) represents an error and the right option (Right) represents a successful result.
  • The use of flatMap, map and cata methods makes it easy to compose a sequence of operations without resorting to nested try/catch.

Further areas of improvement:

  • It is possible to extend functionality by implementing methods to support composition (e.g. flatMap) in order to combine multiple functions that return Either.
  • If the fp-ts library is used, it is possible to further enrich error handling with already ready-made monadic constructs.
  • Adding logging and automated error auditing to CRM will improve business process monitoring.

Conclusion

Error handling is a key aspect of developing stable and scalable CRM systems. Traditional try/catch does not always provide the necessary typing and separation of errors, which can lead to complexity in code maintenance. Using the Either pattern with the Monet library demonstrates how error handling can be made declarative, easily componentized, and robust. The example above shows how a client data update function can be implemented by explicitly separating successful outcomes and error situations. This functional approach simplifies the scaling and integration of system components and makes the code more transparent and manageable.

Popular Posts

My most popular posts

Maximum productivity on remote job
Business

Maximum productivity on remote job

I started my own business and intentionally did my best to work from anywhere in the world. Sometimes I sit with my office with a large 27-inch monitor in my apartment in Cheboksary. Sometimes I’m in the office or in some cafe in another city.

Hello! I am Sergey Emelyanov and I am hardworker
Business PHP

Hello! I am Sergey Emelyanov and I am hardworker

I am a programmer. I am an entrepreneur in my heart. I started making money from the age of 11, in the harsh 90s, handing over glassware to a local store and exchanging it for sweets. I earned so much that was enough for various snacks.

Hire Professional CRM developer for $25 per hour

I will make time for your project. Knowledge of Vtiger CRM, SuiteCRM, Laravel, Vue.js, Wordpress. I offer cooperation options that will help you take advantage of external experience, optimize costs and reduce risks. Full transparency of all stages of work and accounting for time costs. Pay only development working hours after accepting the task. Accept PayPal and Payoneer payment systems. How to hire professional developer? Just fill in the form

Telegram
@sergeyem
Telephone
+4915211100235
Email