Функциональная обработка ошибок в Javascript с помощью Either

Функциональная обработка ошибок в Javascript с помощью Either

Тема обработки ошибок в Javascript возникает практически у всех. Замечу, что на эту тему уже написано довольно большое количество статей. Поэтому я не буду вдаваться в детали и основы. А предложу свой вариант обработки ошибок.

Для начала напомню, что ошибка в Javascript - это по сути "выбрасывание исключения". В дальнейшем исключение требуется обработать приложением. По умолчанию исключение выбрасывает объект Error.

Традиционный подход с использованием try/catch имеет свои недостатки: отсутствие строгой типизации в блоке catch, смешение технических сбоев и ошибок бизнес-логики, а также частые случаи, когда разработчики забывают обрабатывать ошибки в критичных местах. Эту проблему можно решить с помощью паттерна Either, который позволяет явно разделять успешные и ошибочные результаты. В данной статье мы расскажем, как реализовать обработку ошибок в TypeScript, создавая кастомные исключения, и покажем, как с использованием библиотеки Monet можно использовать функциональный подход для работы с ошибками.

1. Обработка ошибок стандартным способом

Обычный способ обработки ошибок—использование блоков try/catch. Однако в блоке catch отсутствует типизация ошибки (тип по умолчанию any), что может привести к затруднениям при дальнейшем анализе исключения. Кроме того, трудно отделить технические ошибки (например, ошибки подключения к базе) от ошибок бизнес-логики.

 

Чтобы решить эту проблему, зачастую пишут кастомные ошибки. Пример создания собственной ошибки:

 

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");
  }
  
}

 

Данный подход позволяет более явно определять тип ошибки, однако сам блок catch продолжает работать с типом any, что не всегда удобно.

Вот как в этом случае будет выглядеть обработка ошибки:

 

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 также поддерживают вложенность. В примере ниже мы ловим ошибку, оборачиваем её в нашу собственную, сохраняя контекст и добавляя более понятное описание.

 

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;
    }
}

 

Очень часто бывает сложно выделить технические ошибки от ошибок бизнес логики. Рассмотрим следующий пример, где мы после обработки запроса по API выбрасываем ошибку, тем самым на уровне выше мы не можем точно понять, произошла проблема техническая или на уровне бизнес-логики.

 

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 и преимущества функциональной обработки ошибок

Паттерн Either широко используется в функциональном программировании. Его суть в том, что функция возвращает не исключение, а результат, который может быть либо успешным (Right), либо ошибочным (Left). Такой подход схож с обработкой ошибок в Go, где ошибки возвращаются вместе с результатом, а не выбрасываются.

 

Преимущества Either:

  • Ясное разделение успешного исхода и ошибки. 
  • Возможность композиции нескольких функций без необходимости множества try/catch блоков. 
  • Прозрачность обработки ошибок — результат явно отражает, успешна операция или нет.

3. Пример реализации обработки ошибок с использованием Either

В этом примере мы реализуем функцию обновления email-клиента в CRM. Для работы с Either мы используем библиотеку Monet. Если при обновлении обнаруживается, что клиент не существует или передан некорректный email, функция возвращает Either.left с соответствующей ошибкой. В противном случае – Either.right с обновлёнными данными клиента.

Пример кода:

 

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)
);

 

В данном примере:

  • Функция updateClientEmail возвращает Either. 
  • Если возникает ошибка (например, клиент не найден или email некорректен), возвращается Either.Left с объектом ошибки. 
  • Если всё прошло успешно, возвращается Either.Right с обновлённым объектом клиента. 
  • Метод cata позволяет "раскладывать" Either и выполнить одну из функций, в зависимости от успешного или ошибочного результата.

4. Пояснения и возможности дальнейшего усовершенствования

Как работает Either:

  • Левый вариант (Left) представляет ошибку, а правый (Right) – успешный результат. 
  • Использование методов flatMap, map и cata позволяет легко компонировать последовательность операций, не прибегая к вложенным try/catch.

Дальнейшие направления улучшения:

  • Можно расширить функциональность, реализовав методы для поддержки композиции (например, flatMap) с целью объединения нескольких функций, возвращающих Either. 
  • При использовании библиотеки fp-ts возможно дополнительное обогащение обработки ошибок за счёт уже готовых монадических конструкций. 
  • Добавление логирования и автоматизированного аудита ошибок в CRM позволит улучшить мониторинг бизнес-процессов.

Заключение

Обработка ошибок – ключевой аспект разработки стабильных и масштабируемых CRM-систем. Традиционный try/catch не всегда позволяет обеспечить необходимую типизацию и разделение ошибок, что может привести к сложности в сопровождении кода. Использование паттерна Either с библиотекой Monet демонстрирует, как можно сделать обработку ошибок декларативной, легко компонуемой и надежной. Приведенный пример показывает, как можно реализовать функцию обновления данных клиента, явно разделив успешный исход и ошибочные ситуации. Такой функциональный подход упрощает масштабирование и интеграцию компонентов системы, делает код более прозрачным и управляемым.

Популярное

Самые популярные посты

Как быть максимально продуктивным на удалённой работе?
Business

Как быть максимально продуктивным на удалённой работе?

Я запустил собственный бизнес и намеренно сделал всё возможное, чтобы работать из любой точки мира. Иногда я сижу с своём кабинете с большим 27-дюймовым монитором в своей квартире в г. Чебоксары. Иногда я нахожусь в офисе или в каком-нибудь кафе в другом городе.

Привет! Меня зовут Сергей Емельянов и я трудоголик
Business PHP

Привет! Меня зовут Сергей Емельянов и я трудоголик

Я программист. В душе я предприниматель. Я начал зарабатывать деньги с 11 лет, в суровые 90-е годы, сдавая стеклотару в местный магазин и обменивая её на сладости. Я зарабатывал столько, что хватало на разные вкусняшки.

Акция! Профессиональный разработчик CRM за 2000 руб. в час

Выделю время под ваш проект. Знания технологий Vtiger CRM, SuiteCRM, Laravel, Vue.js, Golang, React.js, Wordpress. Предлагаю варианты сотрудничества, которые помогут вам воспользоваться преимуществами внешнего опыта, оптимизировать затраты и снизить риски. Полная прозрачность всех этапов работы и учёт временных затрат. Оплачивайте только рабочие часы разработки после приемки задачи. Экономьте на платежах по его содержанию разработчика в штате. Возможно заключение договора по ИП. С чего начать, чтобы нанять профессионального разработчика на full-time? Просто заполните форму!

Telegram
@sergeyem
Telephone
+4915211100235
Email