В моей программе, я вывожу forks репозитория, который вбиваю вручную.
Естественно хочу, в случае, если допущена ошибка ввода, обработать ее, например, вывести сообщение, что данный репозиторий не найден, и чтобы в консоли не было никаких сообщений об ней.
Вот так выглядит рабочий пример без обработки ошибок:
fetch(`https://api.github.com/repos/gudh/ihover/forks`)
.then(res => res.json())
.then(data => console.log('+', data))
А вот здесь я пытаюсь обработать ошибку, чтобы понять, где мне писать код, который будет выводить сообщение о том, что не найден репозиторий, я вызываю console.log
, но ни .catch
ни try
не вызывают console.log
, и все равно ошибка падает в консоль.
try {
fetch(`https://api.github.com/repos/gudh/ihove/forks`)
.then(res => res.json())
.then(data => console.log('+', data))
.catch(() => console.log(12));
} catch (e) {
console.log(132)
}
Если откроете консоль браузера, увидите, что туда попала ошибка:
Время на прочтение
4 мин
Количество просмотров 10K
Если вы пришли сюда только ради ответа и вам не интересны рассуждения — листайте вниз
Как все начиналось
Для начала, давайте вспомним, а как вообще ловят ошибки в 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
, мы можем возвращать свои обертки и успешно их использовать, в зависимости от наших убеждений.
Очень интересно мнение сообщества на эту тему.
Fetch API используется для выполнения ajax-запросов, для вызова API или выборки удаленного ресурса или получения HTML-файла с сервера.
Базовый синтаксис Fetch API
Допустим, нужно получить список сообщений из API https://jsonplaceholder.typicode.com/posts. Во-первых, следует передать этот URL в метод fetch()
в качестве аргумента.
fetch('https://jsonplaceholder.typicode.com/posts');
Метод fetch()
возвращает Promise. Поэтому ответы API нужно обрабатывать с использованием then()
и catch()
.
fetch('https://jsonplaceholder.typicode.com/posts')
.then(function (response) {
// API вернул результат
console.log('success!', response);
}).catch(function (err) {
// Что-то пошло не так
console.warn('Ошибка получения.');
console.log(err);
});
Посмотрите пример.
Посмотрим на ответ в консоли: сразу можно обратить внимание, в response.body
нет JSON. Это – ReadableStream
.
Fetch API использует потоки. Чтобы получить данные из API в виде объекта JSON, можно использовать оригинальный метод Fetch API: json()
.
fetch('https://jsonplaceholder.typicode.com/posts')
.then(function (response) {
// API вернул результат
return response.json();
}).then(function (data) {
// Это уже JSON
console.log(data);
}).catch(function (err) {
// Что-то пошло не так
console.warn('Ошибка получения.');
console.log(err);
});
Посмотрите, теперь тут нормальный JSON.
Обработка ошибок для Fetch API
Fetch API обрабатывает ошибки с помощью метода catch()
, потому что он возвращает Promise.
Однако, Promise отклоняется и вызывает метод catch()
только в случае сбоя. Если вернулся ответ от сервера, даже в случае с ошибками 404 или 500, будут работать методы then()
.
Чтобы пофиксить такое поведение, можно использовать свойство ok
в response
, который возвращает Promise.
Если у свойства response.ok
значение true
, вернём response.json()
. Если нет, то вернём отклоненный объект Promise и передадим в нём ответ, чтобы вызвать метод catch()
.
fetch('https://jsonplaceholder.typicode.com/postsQQQQ')
.then(function (response) {
// API вернул результат
if (response.ok) {
return response.json();
} else {
console.log('Это не 200OK')
return Promise.reject(response);
}
}).then(function (data) {
// Это уже JSON
console.log(data);
}).catch(function (err) {
// Что-то пошло не так
console.warn('Ошибка получения.');
console.log(err);
});
Так лучше обрабатывать ошибки в методе Fetch.
В этом примере умышленно искажён URL, он вернёт 404 ошибку, чтобы продемонстрировать отклонение Promise вручную для принудительного вызова метода catch()
.
То же самое можно делать с помощью XHR
Обработчик onreadystatechange
в XHR тоже выполняется независимо от того, был ответ успешным или нет. Здесь тоже нужно проверять, находится ли ответ между 200 и 300, прежде чем работать с ним.
xhr.onreadystatechange = function () {
// Вернём, когда работа метода завершится
if (xhr.readyState !== 4) return;
// обработаем ответ
if (xhr.status >= 200 && xhr.status < 300) {
// Ответ нормальный
console.log('success', JSON.parse(xhr.responseText));
} else {
// Ответ плохой
console.log('error', xhr);
}
};
У Fetch API код получился намного чище, сравните.
Создание других типов запросов
По умолчанию Fetch API выполняет GET-запросы. Допустим, нужно сделать POST-запрос для публикации новой статьи через API. Методу fetch()
надо в этом случае передавать второй аргумент, который можно использовать для передачи объекта с параметрами.
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST'
}).then(function (response) {
// API вернул результат
if (response.ok) {
return response.json();
} else {
console.log('Это не 200OK')
return Promise.reject(response);
}
}).then(function (data) {
// Это уже JSON
console.log(data);
}).catch(function (err) {
// Что-то пошло не так
console.warn('Ошибка получения.');
console.log(err);
});
Для определенных типов запросов может понадобиться передавать данные вместе с запросом. Это можно сделать с помощью свойства body в объекте параметров.
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: 'title=' + encodeURIComponent('Заголовок') +
'&body=' + encodeURIComponent('Текст статьи')
}).then(function (response) {
// API вернул результат
if (response.ok) {
return response.json();
} else {
console.log('Это не 200OK')
return Promise.reject(response);
}
}).then(function (data) {
// Это уже JSON
console.log(data);
}).catch(function (err) {
// Что-то пошло не так
console.warn('Ошибка получения.');
console.log(err);
});
Установка заголовков с помощью Fetch API
Еще одна распространенная вещь, которую может потребоваться сделать, установить для запроса заголовки и другие свойства. Это также можно сделать с помощью объекта параметров.
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: 'title=' + encodeURIComponent('Заголовок') +
'&body=' + encodeURIComponent('Текст статьи'),
headers: {
'Content-Type': 'application/json'
},
referrer: 'no-referrer'
}).then(function (response) {
// API вернул результат
if (response.ok) {
return response.json();
} else {
console.log('Это не 200OK')
return Promise.reject(response);
}
}).then(function (data) {
// Это уже JSON
console.log(data);
}).catch(function (err) {
// Что-то пошло не так
console.warn('Ошибка получения.');
console.log(err);
});
Демка
Здесь можно поэкспериментировать с кросс-доменными запросами (CORS).
See this code cors fetch on x.xhtml.ru.
How to use the Fetch API with vanilla JS
I really liked @tjvantoll article Handling Failed HTTP Responses With fetch(). The one thing I found annoying with it, though, is that response.statusText
always returns the generic error message associated with the error code. Most APIs, however, will generally return some kind of useful, more human friendly message in the body.
Here’s a modification that will capture this message. The key is that rather than throwing an error, you just throw the response and then process it in the catch
block to extract the message in the body:
fetch("/api/foo")
.then( response => {
if (!response.ok) { throw response }
return response.json() //we only get here if there is no error
})
.then( json => {
this.props.dispatch(doSomethingWithResult(json))
})
.catch( err => {
err.text().then( errorMessage => {
this.props.dispatch(displayTheError(errorMessage))
})
})
Frankly, I’m horrified that JavaScript let’s you throw some random value, rather than an error, but hey, when in Rome…
Начало: «JavaScript: простой пример работы с методом fetch».
В прошлом посте я разбирал, как заставить работать пример с встроенным методом fetch
из подраздела 3.1 «Fetch» третьего раздела («Сетевые запросы») третьей части («Тематические разделы») учебника по JavaScript.
В итоге у меня получился такой код:
(async () => { let url = "https://api.github.com"; let response = await fetch(url); if (response.ok) { // если HTTP-статус в диапазоне 200-299 // получаем тело ответа let json = await response.json(); console.log(json); } else { alert("Ошибка HTTP: " + response.status); } })();
Можно было заметить, что, несмотря на вроде бы наличие ветки с обработкой ошибок, скрипт всё равно при некоторых ошибках (эти ошибки в консоли разработчика содержат в своём названии слово «Uncaught», что по-русски означает «Непойманная» [в смысле «непойманная ошибка»]) прекращает работу. (Я писал об этом отдельный пост: «JavaScript: слово Uncaught в названии ошибки».)
На самом деле, в вышеприведенном коде ветка с обработкой ошибок обрабатывает только ошибки, которые возникнут на веб-сервере при обработке запроса, который мы в данном коде посылаем на веб-сервер (ошибкой наш скрипт считает все ответы веб-сервера с кодом, выходящим за пределы диапазона 200-299).
Ну а непойманная ошибка в данном случае может возникнуть до того, как исполнение скрипта дойдет до ветки с обработкой ошибок, при выполнении одной из двух асинхронных задач. Либо тут (выполнение первой асинхронной задачи):
let response = await fetch(url);
либо тут (выполнение второй асинхронной задачи):
let json = await response.json();
Если случается непойманная скриптом ошибка, скрипт прекращает работу. При обучении или отладке мы временно можем себе это позволить, так как у нас при этом есть более важные задачи: при обучении более важная задача — это понять иллюстрируемые данным примером кода некие принципы программирования или решение каких-то задач (например, научиться работать с встроенным методом fetch
, как в обсуждаемой статье учебника); а при отладке более важная задача — заставить код сначала хоть как-то работать.
Но в скрипте, как окончательном продукте, мы не можем себе позволить непойманных ошибок. Ведь в скрипте при возникновении непойманной ошибки могут еще быть невыполненные инструкции, то есть скрипт на момент такой ошибки, возможно, еще не успел сделать какие-то важные действия. Пользователю при этом никаких ошибок не показывается, скрипт просто «умирает» (если пользователь продвинутый, он, конечно, может открыть консоль разработчика в браузере [если скрипт был запущен в браузерном окружении] и увидеть сообщение о непойманной ошибке). Так что все возможные непойманные ошибки должны быть отловлены.
Как это сделать в данном случае? Изменим код (я отметил изменение красным цветом):
(async () => { let url = "https://api.github.com"; let response = await fetch(url); if (response.ok) { // если HTTP-статус в диапазоне 200-299 // получаем тело ответа let json = await response.json(); console.log(json); } else { alert("Ошибка HTTP: " + response.status); } })().catch(error => alert(error));
Этот метод catch
в данном случае отловит ошибку при выполнении любой из двух асинхронных задач. Естественно, вместо простого вывода сообщения об ошибке на экран с помощью встроенной функции alert
, можно написать какую-то более сложную обработку ошибки.
Подробнее об обработке ошибок при работе с промисами можно почитать следующие статьи учебника из первой части учебника:
Потребители: then, catch, finally
https://learn.javascript.ru/promise-basics#potrebiteli-then-catch-finally
Промисы: обработка ошибок
https://learn.javascript.ru/promise-error-handling
Обработка ошибок при использовании синтаксиса «async/await»
https://learn.javascript.ru/async-await#obrabotka-oshibok