Изоморфная обработка ошибок

Последнее обновление: 01.09.2023

Изоморфная разработка в JavaScript предполагает переход к универсальной системе управлению исключениями, которая способна единообразно перехватывать, логировать и обрабатывать ошибки вне зависимости от выполняемых функций (если эти функции следуют некоторому общему протоколу) и предполагает стандартизацию формата ошибок. Ее цель — снизить дублирование кода, улучшить сопровождение и обеспечить предсказуемое поведение при возникновении сбоев. Дело в том, что обработка ошибок с помощью конструкции try...catch может показаться громоздкой, особенно когда приходится писать вложенные блоки try..catch. С блоками try...catch связан ряд проблем. Например, соответствующие блоки захватывают создаваемые в этих блоках значения, соответственно приходится выносить значения во внешние переменные. А глубокая вложенность функций, которые также могут генерировать исключения, усложняют и код, и понимание того, что он делает.

Рассмотрим простейший пример:

// определение обертки для обработки ошибок
function tryCatch (fn){
  try{
    return [fn(), null];
  }
  catch(err){
    return [null, err];
  }
}

// использование
const sqrt = (x) => {
  if(x < 0) throw new Error(`Number ${x} is invalid`);
  return Math.sqrt(x);
}

// пример получения ошибки
const [res1, err1] = tryCatch(()=>sqrt(-4));

if(err1) console.error(err1);
else console.log("sqrt(-4):", res1);

// пример получения результата
const [res2, err2] = tryCatch(()=>sqrt(4));

if(err2) console.error(err2);
else console.log("sqrt(4):", res2);

Этот код демонстрирует обёртку над функцией для безопасного выполнения с возвратом массива [результат, ошибка] вместо ручного использования try/catch.

Вначале определяется функция tryCatch:

function tryCatch (fn){
  try{
    return [fn(), null];
  }
  catch(err){
    return [null, err];
  }
}

tryCatch через параметр fn принимает другую функцию и вызывает ее внутри блока try/catch. В качестве результата возвращается массив с двумя элементами.

Если функция fn выполняется без ошибок, то возвращается массив [результат, null].

Если происходит ошибка — возвращается [null, ошибка].

Это делает вызовы функций более чистыми, особенно при работе с возможными исключениями.

Далее для примера определена обычная функция sqrt, которая вычисляет квадратный корень переданного в нее числа с помощью встроенной функции Math.sqrt() и которая генерирует ошибку при отрицательных значениях (так как нельзя вычислить квадратный корень для отрицательных чисел).

const sqrt = (x) => {
  if (x < 0) throw new Error(`Number ${x} is invalid`);
  return Math.sqrt(x);
};

Далее идут примеры вызова. В первом случае вызывается sqrt(-4), что вызывает исключение:

const [res1, err1] = tryCatch(()=>sqrt(-4));

Здесь tryCatch ловит исключение и возвращает массив [null, Error("Number -4 is invalid")]

Во втором случае вызов sqrt(4) возвращает 2, соответственно ошибки нет

const [res2, err2] = tryCatch(()=>sqrt(4));;

Фактически возвращается массив [2, null]

В итоге мы получим следующий вывод в консоли браузера:

Error: Number -4 is invalid
sqrt(4): 2

Возьмем другой пример - создание объекта класса:

class Person{
  
    constructor(name, age){
        if(age < 0) throw `Недопустимый возраст ${age}. Минимальное значение: 1`;
        if(name.length < 2) throw `Недопустимое имя ${name}: минимальная длина имени - 2 символа`;
        this.name = name;
        this.age = age;
    }
    print(){ console.log(`Name: ${this.name}  Age: ${this.age}`);}
}

В данном случае класс Person генерирует исключение, если в конструктор передан недопустимый возраст - меньше 0 или если длина переданного имени меньше 2 символов. Используем вышеопределенную функцию tryCatch для обработки исключений:

function tryCatch (fn){
  try{
    return [fn(), null];
  }
  catch(err){
    return [null, err];
  }
}

class Person{
  
    constructor(name, age){
        if(age < 0) throw `Недопустимый возраст ${age}. Минимальное значение: 1`;
        if(name.length < 2) throw `Недопустимое имя ${name}: минимальная длина имени - 2 символа`;
        this.name = name;
        this.age = age;
    }
    print(){ console.log(`Name: ${this.name}  Age: ${this.age}`);}
}

// для примера пытаемся создать пару объектов
const [tom, err1] = tryCatch(() => new Person("Tom", -123));
if(err1) console.error(err1);
else tom.print();

const [bob, err2] = tryCatch(() => new Person("Bob", 46));
if(err2) console.error(err2);
else bob.print();

Здесь для примера пытаемся создать два объекта Person, передавая в вызов tryCatch соответствующий вызов конструктора. И опять же мы получаем массив, где первый элемент - результат, а второй - информация об ошибке. Поскольку в первом случае в конструктор Person передаются заведомо некорректные данные, то мы получим ошибку, а во втором случае - объект Person:

Недопустимый возраст -123. Минимальное значение: 1
Name: Bob  Age: 46

Стоит отметить, что выше представлена только одна из возможных реализаций. Некоторые особенности могут отличаться. Например, функция-обертка может дополнительно обрабатывать ошибку. Так, стандартный тип для ошибок в JavaScript - это тип Error. Однако в коде разработчики не всегда генерируют ошибки этого типа. Например, в коде выше в конструкторе класса Person ошибка по сути представляет простую строку:

if(age < 0) 
  throw `Недопустимый возраст ${age}. Минимальное значение: 1`;  // ошибка представляет строку

И функция-обертка может проверять тип и при необходимости обертывать ошибку в тип Error:

function tryCatch (fn){
  try{
    return [fn(), null];
  }
  catch(err){
    // проверяем, представляет ли ошибка err объект Error
    const error = err instanceof Error ? err : new Error(String(err))
    return [null, error];
  }
}

Также сама функция-обертка может отличаться. Так, мы могли бы определить следующую функцию:

const tryWrap = (fn) => (...args) => {
  try{
    return [fn(...args), null];
  }
  catch(err){
    return [null, err];
  }
}

tryWrap определена как стрелочная функция, которая через параметр fn принимает другую функцию и возвращает новую функцию. Возвращаемая функция вызывает функцию fn с переданными аргументами (...args), но делает это внутри блока try/catch. Обратите внимание, что через параметр args можно передать произвольное количество аргументов. В остальном логика та же самая, что и у выше определнной функции tryCatch: если функция fn выполняется без ошибок, то возвращается массив [результат, null]. Если происходит ошибка, то возвращается массив [null, ошибка].

Пример использования:

// на примере функции sqrt
const [res1, err1] = tryWrap(sqrt)(-4);
const [res2, err2] = tryWrap(sqrt)(4);

// на примере конструктора класса Person
const [tom, tomErr] = tryWrap((name, age)=>new Person(name, age))("Tom", -123);
const [bob, bobErr] = tryWrap((name, age)=>new Person(name, age))("Bob", 46);

Можно в принципе не определять функцию-обертку, если используемые функции изначально поддерживают требуемые контракт - возвращают информацию о результате функции и ошибке в требуемом формате. Например:

// функция возвращает данные в формате "[результат, ошибка]""
const sqrt = (x) => {
  if(x < 0) return [null, new Error(`Number ${x} is invalid`)];
  return [Math.sqrt(x), null];
}

const [res1, err1] = sqrt(-4);
if(err1) console.error(err1);
else console.log("sqrt(-4):", res1);


const [res2, err2] = sqrt(4);
if(err2) console.error(err2);
else console.log("sqrt(4):", res2);

Таким образом мы получили унифицированный способ для обработки ошибок, и нам не надо писать try/catch каждый раз при вызове отдельной функции, которая может сгенерировать ошибку. Особенно данный подход упрощает написание кода в функциональном стиле. Для асинхронных операций этот подход можно адаптировать в виде async/await-совместимого варианта.

В итоге счет изоморфной обработки ошибок упрощается код, улучшается его отладка и мониторинг благодаря централизованной обработке ошибок, упрощается повторное использование код и повышается надёжности приложения за счёт предсказуемой реакции на сбои.

Помощь сайту
Юмани:
410011174743222
Номер карты:
4048415020898850