Как стать автором
Обновить

Как в современном мире обрабатывать ошибки в Javascript?

Уровень сложности Простой
Время на прочтение 4 мин
Количество просмотров 12K

Если вы пришли сюда только ради ответа и вам не интересны рассуждения - листайте вниз (в комментарии) :)

Как все начиналось

Для начала, давайте вспомним, а как вообще ловят ошибки в js, будь то браузер или сервер. В js есть конструкция try...catch.

try {
    let data = JSON.parse('...');
} catch(err: any) {
		// если произойдет ошибка, то мы окажемся здесь
}

Это общепринятая конструкция и в большинстве языков она есть. Однако, тут есть проблема (и как окажется дальше - не единственная), эта конструкция "не будет работать" для асинхронного кода, для кода который был лет 5 назад. В те времена, в браузере использовали для Ajax запроса XMLHttpRequest.

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com', true);
xhr.addEventListener('error', (e: ProgressEvent<XMLHttpRequestEventTarget>) => {
    // если произойдет ошибка, то мы окажемся здесь
});

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

В NodeJS с самого начала продвигалась концепция Error-First Callback, эта идея применялась для асинхронных функций, например, для чтения файла. Смысл ее в том, чтобы первым аргументом передавать в функцию обратного вызова ошибку, а следующими аргументами уже получаемые данные.

import fs from 'fs';

fs.readFile('file.txt', (err, data) => {
    if (err) {
        // обработка ошибки
    }
    // если все хорошо, работаем с данными
});

Если мы посмотрим какой тип имеет переменная err, то увидим следующее:

interface ErrnoException extends Error {
    errno?: number | undefined;
    code?: string | undefined;
    path?: string | undefined;
    syscall?: string | undefined;
}

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

Через некоторое время, в Javascript появились Promise. Они, безусловно, изменили разработку на js к лучшему. Ведь никто* никто не любит городить огромные конструкции из функций обратного вызова.

fetch('https://api.example.com')
  .then(res => {
    // если все хорошо, работаем с данными
  })
  .catch(err => {
		// обработка ошибки
  });

Несмотря на то, что внешне этот пример сильно отличается от первого, тем не менее, мы видим явную логическую связь. Очевидно, что разработчики хотели сделать похожую на try...catch конструкцию. Со временем, появился еще один способ обработать ошибку в асинхронном коде. Этот способ, по сути, является лишь синтаксическим сахаром для предыдущего примера.

try {
  const res = await fetch('https://api.example.com');
  // если все хорошо, работаем с данными
} catch(err) {
	// обработка ошибки
}

Также, конструкция try...catch позволяет ловить ошибки из нескольких промисов одновременно.

try {
  let usersRes = await fetch('https://api.example.com/users');
	let users = await usersRes.json();

  let chatsRes = await fetch('https://api.example.com/chats');
	let chats = await chatsRes.json();

  // если все хорошо, работаем с данными
} catch(err) {
	// обработка ошибки
}

Вот, замечательный вариант ловли ошибок. Любая ошибка которая возникнет внутри блока try, попадет в блок catch и мы точно её обработаем.

А точно ли обработаем?

Действительно, а правда ли, что мы обработаем ошибку, или всего лишь сделаем вид? На практике, скорее всего, возникнувшая ошибка будет просто выведена в консоль или т.п. Более того, при появлении ошибки*, интерпретатор прыгнет в блок catch , где не мы, не TypeScript не сможет вывести тип переменной, попавшей туда (пример - возврат с помощью Promise.reject), после чего, произойдет выход из функции. То есть, мы не сможем выполнить код который находится в этом же блоке, но который расположен ниже функции, внутри которой произошла ошибка. Конечно, мы можем предусмотреть такие ситуации, но сложность кода и читаемость вырастут многократно.

Как быть?

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

let [users, err] = await httpGET('https://api.example.com/users');
if (err !== null) {
	// обработка ошибки
}
// продолжаем выполнение кода

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

Пример для вызова нескольких функций возвращающих Promise.

let err: Error,
		users: User[],
		chats: Chat[];

[users, err] = await httpGET('https://api.example.com/users');
if (err !== nil) {
  // обработка ошибки
}

[chats, err] = await httpGET('https://api.example.com/chats');
if (err !== nil) {
  // обработка ошибки
}

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

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

type PairPromise<T> = Promise<[T, null] | [null, Error]>;

Определим возможные возвращаемые ошибки.

const notFoundError = new Error('NOT_FOUND');
const serviceUnavailable = new Error('SERVICE_UNAVAILABLE');

Теперь опишем нашу функцию.

const getUsers = async (): PairPromise<User[]> => {
    try {
        let res = await fetch('https://api.example.com/users');
        if (res.status === 504) {
            return Promise.resolve([null, serviceUnavailable]);
        }

        let users = await res.json() as User[];

        if (users.length === 0) {
            return Promise.resolve([null, notFoundError]);
        }

        return Promise.resolve([users, null]);
    } catch(err) {
        return Promise.resolve([null, err]);
    }
} 

Пример использования такой функции.

let [users, err] = await getUsers();
if (err !== null) {
	switch (err) {
  	case serviceUnavailable:
    	// сервис недоступен
    case notFoundError:
    	// пользователи не найдены
    default:
    	// действие при неизвестной ошибке
	}
}

Вариантов применения данного подхода обработки ошибок очень много. Мы сочетаем удобства конструкции try...catch и Error-First Callback, мы гарантированно поймаем все ошибки и сможем удобно их обработать, при необходимости. Как приятный бонус - мы не теряем типизацию. Также, мы не скованы лишь объектом Error, мы можем возвращать свои обертки и успешно их использовать, в зависимости от наших убеждений.

Очень интересно мнение сообщества на эту тему.

Теги:
Хабы:
-2
Комментарии 63
Комментарии Комментарии 63

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн