JavaScript · API · Продвинутый

Обработка ошибок

5 задач

Надёжно обрабатывайте HTTP-ошибки и сбои сети с помощью response.ok и try/catch.

HTTP-ошибки и сбои сети

#
В отличие от `axios`, обычный `fetch` **никогда не бросает исключение при HTTP-ошибках** (4xx, 5xx). Ответ 404 или 500 возвращается нормально — вы сами должны проверить `response.ok`: ```javascript const response = await fetch(url); if (!response.ok) { // 4xx или 5xx — обработайте здесь console.log('Ошибка:', response.status); } else { const data = await response.json(); } ``` `response.ok` равно `true` для 200–299, `false` для всего остального. `fetch` бросает исключение (отклоняет Promise) только при **сбоях сети** — нет интернета, ошибка DNS, запрос прерван: ```javascript try { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); } catch (err) { if (err instanceof TypeError) { console.log('Ошибка сети:', err.message); } else { console.log('HTTP-ошибка:', err.message); } } ``` AbortController позволяет установить таймаут: ```javascript const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); // таймаут 5 секунд try { const response = await fetch(url, { signal: controller.signal }); clearTimeout(timeout); return response.json(); } catch (err) { if (err.name === 'AbortError') console.log('Запрос превысил время ожидания'); else throw err; } ```

Практические паттерны обработки ошибок

#
Обёртка `safeFetch`, обрабатывающая все случаи: ```javascript async function safeFetch(url, options = {}) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); try { const response = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timeout); if (!response.ok) { const body = await response.text(); throw new Error(`HTTP ${response.status}: ${body}`); } return await response.json(); } catch (err) { if (err.name === 'AbortError') { console.error('Запрос превысил время ожидания:', url); } else if (err instanceof TypeError) { console.error('Ошибка сети:', err.message); } else { console.error('Ошибка:', err.message); } return null; } } // Использование const data = await safeFetch('https://apilearn.tukas.dev/api/products/'); if (data) console.log(data.count); ``` Отдельная обработка 401: ```javascript async function fetchWithAuth(url, headers) { const response = await fetch(url, { headers }); if (response.status === 401) { console.log('Не авторизован — проверьте токен'); return null; } if (!response.ok) { console.log(`HTTP-ошибка: ${response.status}`); return null; } return response.json(); } ``` Повтор при 429 (лимит запросов) или 503 (сервис недоступен): ```javascript async function fetchWithRetry(url, options = {}, retries = 3) { for (let attempt = 0; attempt < retries; attempt++) { const response = await fetch(url, options); if ([429, 503].includes(response.status) && attempt < retries - 1) { const wait = 2 ** attempt * 1000; // 1с, 2с, 4с console.log(`Попытка ${attempt + 1} неудачна, повтор через ${wait}мс...`); await new Promise(r => setTimeout(r, wait)); continue; } return response; } } ```

Справочник обработки ошибок

#
**Проверка response.ok (всегда делайте это)** ```javascript const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); ``` **fetch бросает исключение только при сетевых ошибках** ``` TypeError Сбой сети (офлайн, ошибка DNS, блокировка CORS) AbortError AbortController.abort() был вызван (например, таймаут) // 4xx/5xx — НЕ бросается, проверяйте response.ok вручную ``` **Таймаут через AbortController** ```javascript const controller = new AbortController(); setTimeout(() => controller.abort(), 5000); const response = await fetch(url, { signal: controller.signal }); ``` **Паттерн catch** ```javascript try { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (err) { if (err.name === 'AbortError') { /* таймаут */ } else if (err instanceof TypeError) { /* сеть */ } else { /* HTTP-ошибка от нашего throw */ } } ``` **Коды статусов** ``` 200 OK 201 Created 204 No Content (успешное DELETE — без тела) 400 Bad Request 401 Unauthorized 403 Forbidden 404 Not Found 429 Too Many Requests 500 Internal Server Error 503 Service Unavailable ```
01

Проверить response.ok

#

Выполните запрос к `/api/products/99999/` (несуществующий). Выведите `response.status` и `response.ok`. Затем выполните запрос к `/api/products/1/` и сравните.

const BASE_URL = 'https://apilearn.tukas.dev';

async function main() {
  // запрос к несуществующему товару — вывести status и ok
  // запрос к реальному товару — вывести status и ok
}

main();
Решение
const BASE_URL = 'https://apilearn.tukas.dev';

async function main() {
  const r1 = await fetch(`${BASE_URL}/api/products/99999/`);
  console.log(r1.status, r1.ok);   // 404 false

  const r2 = await fetch(`${BASE_URL}/api/products/1/`);
  console.log(r2.status, r2.ok);   // 200 true
}

main();
02

Бросить исключение при HTTP-ошибке

#

Напишите функцию `getProduct(id)`, которая получает товар и бросает `Error`, если `response.ok` равно `false`. Вызовите её с действительным id и с `99999`. Поймайте и выведите сообщение об ошибке.

const BASE_URL = 'https://apilearn.tukas.dev';

async function getProduct(id) {
  // fetch, проверить response.ok, бросить исключение если не ok
}

async function main() {
  // вызвать с действительным id
  // вызвать с 99999 и поймать ошибку
}

main();
Решение
const BASE_URL = 'https://apilearn.tukas.dev';

async function getProduct(id) {
  const response = await fetch(`${BASE_URL}/api/products/${id}/`);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

async function main() {
  try {
    const p = await getProduct(1);
    console.log(p.name);
  } catch (err) {
    console.log('Error:', err.message);
  }

  try {
    await getProduct(99999);
  } catch (err) {
    console.log('Error:', err.message);  // HTTP 404
  }
}

main();
03

Обработать 401 Unauthorized

#

Попробуйте запросить `/api/users/profile/` без токена. Проверьте `response.status` — если это 401, выведите конкретное сообщение. Иначе выведите данные.

const BASE_URL = 'https://apilearn.tukas.dev';

async function main() {
  // запрос профиля без авторизации, отдельная обработка 401
}

main();
Решение
const BASE_URL = 'https://apilearn.tukas.dev';

async function main() {
  const response = await fetch(`${BASE_URL}/api/users/profile/`);
  if (response.status === 401) {
    console.log('Unauthorized — provide a valid token');
  } else if (!response.ok) {
    console.log(`HTTP error: ${response.status}`);
  } else {
    const data = await response.json();
    console.log(data.username);
  }
}

main();
04

Обёртка safeFetch

#

Напишите функцию `safeFetch(url)`, которая проверяет `response.ok` и возвращает `null` (выводя ошибку) при сбое, или разобранный JSON при успехе. Проверьте с действительным URL и URL, возвращающим 404.

const BASE_URL = 'https://apilearn.tukas.dev';

async function safeFetch(url) {
  // fetch, проверить ok, вернуть json или null
}

async function main() {
  const good = await safeFetch(`${BASE_URL}/api/products/1/`);
  console.log(good ? good.name : 'null');

  const bad = await safeFetch(`${BASE_URL}/api/products/99999/`);
  console.log(bad);
}

main();
Решение
const BASE_URL = 'https://apilearn.tukas.dev';

async function safeFetch(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      console.error(`HTTP ${response.status} for ${url}`);
      return null;
    }
    return response.json();
  } catch (err) {
    console.error('Network error:', err.message);
    return null;
  }
}

async function main() {
  const good = await safeFetch(`${BASE_URL}/api/products/1/`);
  console.log(good ? good.name : 'null');

  const bad = await safeFetch(`${BASE_URL}/api/products/99999/`);
  console.log(bad);
}

main();
05

Таймаут через AbortController

#

Напишите функцию `fetchWithTimeout(url, ms)`, которая прерывает запрос, если он выполняется дольше `ms` миллисекунд. Проверьте на обычном запросе и выведите результат или `"timed out"`.

const BASE_URL = 'https://apilearn.tukas.dev';

async function fetchWithTimeout(url, ms = 3000) {
  // использовать AbortController для прерывания через ms миллисекунд
}

async function main() {
  const data = await fetchWithTimeout(`${BASE_URL}/api/products/`, 5000);
  console.log(data ? data.count : 'timed out');
}

main();
Решение
const BASE_URL = 'https://apilearn.tukas.dev';

async function fetchWithTimeout(url, ms = 3000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);
  try {
    const response = await fetch(url, { signal: controller.signal });
    clearTimeout(timer);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('Request timed out');
      return null;
    }
    throw err;
  }
}

async function main() {
  const data = await fetchWithTimeout(`${BASE_URL}/api/products/`, 5000);
  console.log(data ? data.count : 'timed out');
}

main();