Работа с Fetch API в JavaScript: примеры, методы и приемы кода
#Web API #Async/Await #Fetch APIДля кого эта статья:
- Для веб-разработчиков, знакомых с JavaScript
- Для программистов, работающих с асинхронными HTTP-запросами
- Для специалистов, интересующихся современными методами взаимодействия с серверами
Fetch API — мощный инструмент для веб-разработчиков, который произвел настоящую революцию в мире асинхронных HTTP-запросов. Помните те времена, когда мы боролись с бесконечными колбэками и запутанной конфигурацией XMLHttpRequest? 🚀 Fetch API перевернул игру, предоставив элегантный, промис-ориентированный интерфейс, который значительно упрощает взаимодействие с серверами. Независимо от того, разрабатываете ли вы сложное одностраничное приложение или просто добавляете динамическое поведение на статический сайт, понимание Fetch API — это обязательный навык для каждого уважающего себя JavaScript-разработчика. Давайте погрузимся в мир Fetch API и раскроем его потенциал для создания быстрых, эффективных и надежных веб-приложений.
Что такое Fetch API и почему он заменил XMLHttpRequest
Fetch API представляет собой современный JavaScript-интерфейс для выполнения HTTP-запросов, который построен на Promise-архитектуре. Это встроенный в браузер API, разработанный как альтернатива устаревшему XMLHttpRequest (XHR), с более гибким и мощным набором возможностей.
По сути, Fetch API обеспечивает простой и логичный способ асинхронно получать ресурсы по сети, который легко интегрируется с остальной частью JavaScript-экосистемы.
Алексей Ковалев, технический архитектор
В 2018 году я руководил миграцией корпоративной панели управления с jQuery AJAX на нативный Fetch API. Система выполняла тысячи запросов ежедневно для сбора данных с разных микросервисов. Старый код был громоздким: для каждого запроса требовалось более 15 строк кода с обработкой колбэков, проверкой состояний и обработкой ошибок.
После миграции на Fetch API код стал значительно чище. Цепочки промисов позволили структурировать асинхронные операции последовательно, а использование async/await сделало код еще более читаемым. Производительность выросла на 23%, а время отклика системы уменьшилось на 780 мс. Но что действительно впечатлило команду — сокращение кодовой базы на 34% при расширении функциональности.
Переход на Fetch API позволил нам быстрее внедрять новые функции и значительно упростил поддержку приложения. Спустя три года никто из разработчиков не испытывает ностальгии по XMLHttpRequest.
Рассмотрим ключевые преимущества Fetch API по сравнению с XMLHttpRequest:
| Аспект | XMLHttpRequest | Fetch API |
|---|---|---|
| Асинхронность | Основан на колбэках | Использует Promises и async/await |
| Синтаксис | Многословный, требует много кода | Лаконичный, цепочки методов |
| Обработка ошибок | Сложная, через проверку состояний | Структурированная через catch |
| Поддержка стримов | Отсутствует | Встроенная поддержка Response.body |
| Поддержка CORS | Ограниченная | Расширенная, с детальными настройками |
| Прерывание запросов | Нестандартизированное | AbortController API |
Основные причины перехода от XMLHttpRequest к Fetch API:
- Промисы вместо колбэков — современная обработка асинхронных операций
- Модульность и расширяемость — разделение на Request, Response и Headers объекты
- Потоковая передача данных — возможность работать с частичными ответами
- Нативная поддержка JSON — встроенные методы для работы с JSON
- Простота использования — меньше кода для выполнения тех же задач
Fetch API сегодня поддерживается всеми современными браузерами, включая мобильные версии. Для обеспечения поддержки в устаревших браузерах можно использовать полифиллы, но большинство проектов уже могут полагаться на нативную реализацию.

Основные методы и настройки fetch для HTTP-запросов
Базовый синтаксис метода fetch() выглядит следующим образом:
fetch(resource, options)
.then(response => /* обработка ответа */)
.catch(error => /* обработка ошибки */);
Где resource — это URL, к которому выполняется запрос, а options — объект с настройками запроса. Рассмотрим основные параметры конфигурации:
| Параметр | Описание | Значение по умолчанию | Примеры значений |
|---|---|---|---|
| method | HTTP-метод запроса | GET | POST, PUT, DELETE, PATCH |
| headers | Заголовки HTTP-запроса | {} | {'Content-Type': 'application/json'} |
| body | Тело запроса | undefined | JSON.stringify(data), FormData, Blob |
| mode | Режим CORS | cors | same-origin, no-cors |
| credentials | Передача куки | same-origin | include, omit |
| cache | Кэширование | default | no-cache, reload, force-cache |
| redirect | Обработка редиректов | follow | error, manual |
| referrer | Реферер запроса | client/about:client | no-referrer, URL |
| signal | Сигнал для прерывания | undefined | AbortController.signal |
Ключевые объекты, с которыми работает Fetch API:
- Request — представляет HTTP-запрос
- Response — представляет HTTP-ответ
- Headers — представляет HTTP-заголовки
- Body — предоставляет методы для работы с телом запроса/ответа
Наиболее распространенные методы для работы с данными в ответе:
// Получение ответа в виде JSON
response.json()
// Получение ответа в виде текста
response.text()
// Получение ответа в виде Blob
response.blob()
// Получение ответа в виде FormData
response.formData()
// Получение ответа в виде ArrayBuffer
response.arrayBuffer()
Важно помнить: методы извлечения тела (json(), text() и т.д.) возвращают промисы и могут быть вызваны только один раз. При повторных вызовах возникнет ошибка, так как тело уже было использовано.
GET и POST запросы с fetch: синтаксис и практика
Самые распространенные типы HTTP-запросов — GET для получения данных и POST для их отправки. Рассмотрим, как правильно реализовать эти операции с помощью Fetch API.
🔍 GET-запрос — самый простой вариант использования fetch:
// Базовый GET-запрос
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`Ошибка HTTP: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Проблема с запросом:', error));
// GET-запрос с параметрами в URL
const params = new URLSearchParams({
category: 'electronics',
sort: 'price',
order: 'desc'
});
fetch(`https://api.example.com/products?${params}`)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Ошибка:', error));
✉️ POST-запрос — требует дополнительной конфигурации для отправки данных:
// POST-запрос с JSON данными
const userData = {
username: 'developer2023',
email: 'dev@example.com',
age: 28
};
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your_token_here'
},
body: JSON.stringify(userData)
})
.then(response => response.json())
.then(data => console.log('Успешно создан пользователь:', data))
.catch(error => console.error('Ошибка:', error));
Более современный подход с использованием async/await делает код еще чище и понятнее:
// GET-запрос с async/await
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`Ошибка HTTP: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Проблема с получением данных:', error);
throw error; // Пробрасываем ошибку дальше
}
}
// POST-запрос с async/await
async function createUser(userData) {
try {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `HTTP ошибка! Статус: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Ошибка при создании пользователя:', error);
throw error;
}
}
Мария Сорокина, фронтенд-разработчик
Недавно я оптимизировала поисковую систему для каталога продуктов с более чем 50,000 товаров. Изначально поиск работал с задержкой: каждое нажатие клавиши вызывало новый запрос к API, что приводило к "гонке запросов" и непредсказуемым результатам.
Проблема решилась элегантно с помощью AbortController из Fetch API. Вот что я реализовала:
- При каждом вводе символа создавался новый AbortController
- Предыдущий запрос автоматически отменялся
- Добавила задержку 300мс перед отправкой нового запроса
JSСкопировать кодlet controller; function searchProducts(query) { // Отменяем предыдущий запрос if (controller) { controller.abort(); } // Создаём новый контроллер controller = new AbortController(); return fetch(`/api/search?q=${query}`, { signal: controller.signal }) .then(response => response.json()); } // Использование с debounce const debouncedSearch = debounce(searchProducts, 300); searchInput.addEventListener('input', (e) => { debouncedSearch(e.target.value) .then(renderResults) .catch(err => { if (err.name !== 'AbortError') { console.error(err); } }); });
Результат: снижение нагрузки на сервер на 87%, устранение визуальных "прыжков" интерфейса и повышение точности поиска. Пользователи перестали жаловаться на странное поведение поисковых результатов. Это решение мы впоследствии стандартизировали для всех асинхронных операций в приложении.
При работе с различными типами запросов важно учитывать особенности HTTP-методов:
- GET — не должен содержать body, параметры передаются в URL
- POST — отправляет данные на сервер для создания ресурса
- PUT — заменяет существующий ресурс новым
- PATCH — частично обновляет существующий ресурс
- DELETE — удаляет ресурс на сервере
Обработка ответов и преобразование данных в fetch
После получения ответа от сервера, Fetch API предоставляет разнообразные возможности для обработки и преобразования данных. Объект Response, возвращаемый промисом fetch, содержит всю информацию об ответе и методы для извлечения данных.
Основные свойства Response-объекта:
- response.ok — булево значение, true если статус ответа в диапазоне 200-299
- response.status — числовой код статуса HTTP
- response.statusText — текстовое сообщение статуса
- response.headers — объект Headers с заголовками ответа
- response.url — URL ответа
- response.type — тип ответа (basic, cors, error и т.д.)
- response.redirected — был ли ответ результатом перенаправления
- response.body — ReadableStream с телом ответа
Прежде всего, при работе с Fetch API следует проверять успешность запроса:
fetch('https://api.example.com/data')
.then(response => {
// Проверка успешности запроса
if (!response.ok) {
// Извлекаем информацию об ошибке из тела ответа
return response.json().then(errorData => {
throw new Error(`API вернул ошибку: ${errorData.message || response.status}`);
});
}
return response.json();
})
.then(data => console.log('Данные:', data))
.catch(error => console.error('Ошибка:', error));
Преобразование ответа в разные форматы в зависимости от типа данных:
// Получение JSON
fetch('/api/users')
.then(response => response.json())
.then(users => console.log(users));
// Получение текста (например, HTML или XML)
fetch('/api/document')
.then(response => response.text())
.then(document => console.log(document));
// Получение бинарных данных (например, изображения)
fetch('/api/image.png')
.then(response => response.blob())
.then(imageBlob => {
const imageUrl = URL.createObjectURL(imageBlob);
const imgElement = document.createElement('img');
imgElement.src = imageUrl;
document.body.appendChild(imgElement);
});
// Получение FormData
fetch('/api/form-data')
.then(response => response.formData())
.then(formData => {
for (const [key, value] of formData.entries()) {
console.log(`${key}: ${value}`);
}
});
// Получение ArrayBuffer
fetch('/api/binary-data')
.then(response => response.arrayBuffer())
.then(buffer => {
// Работа с ArrayBuffer
const view = new Uint8Array(buffer);
console.log(view);
});
Работа с HTTP-заголовками:
fetch('https://api.example.com/data')
.then(response => {
// Получение отдельного заголовка
const contentType = response.headers.get('Content-Type');
console.log('Тип контента:', contentType);
// Перебор всех заголовков
console.log('Все заголовки:');
for (const [key, value] of response.headers.entries()) {
console.log(`${key}: ${value}`);
}
// Проверка наличия заголовка
if (response.headers.has('X-Rate-Limit')) {
console.log('Ограничение запросов:', response.headers.get('X-Rate-Limit'));
}
return response.json();
});
Обработка различных типов ошибок в fetch:
| Тип ошибки | Описание | Как обрабатывать |
|---|---|---|
| Ошибки сети | Отсутствие соединения, CORS, недоступный сервер | Перехват в catch |
| HTTP-ошибки (4xx, 5xx) | Неверные параметры, ошибки сервера | Проверка response.ok |
| Ошибки парсинга | Некорректный формат данных (невалидный JSON) | try/catch вокруг методов преобразования |
| AbortError | Запрос был прерван | Проверка error.name === 'AbortError' |
| Таймауты | Слишком долгое ожидание ответа | Комбинация Promise.race и setTimeout |
Пример комплексной обработки ошибок:
async function fetchWithErrorHandling(url) {
try {
// Создаём таймаут для запроса
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Таймаут запроса')), 5000);
});
// Используем Promise.race для ограничения времени ожидания
const response = await Promise.race([
fetch(url),
timeoutPromise
]);
// Проверяем HTTP-статус
if (!response.ok) {
let errorMessage;
try {
// Пробуем извлечь сообщение об ошибке из тела ответа
const errorData = await response.json();
errorMessage = errorData.message || `Ошибка HTTP: ${response.status}`;
} catch (e) {
// Если не удалось распарсить JSON, используем статус
errorMessage = `Ошибка HTTP: ${response.status} ${response.statusText}`;
}
throw new Error(errorMessage);
}
// Пытаемся распарсить JSON
try {
return await response.json();
} catch (e) {
throw new Error('Невалидный формат JSON в ответе');
}
} catch (error) {
// Классифицируем ошибки
if (error.name === 'AbortError') {
console.error('Запрос был прерван');
} else if (error.message === 'Таймаут запроса') {
console.error('Сервер не ответил вовремя');
} else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
console.error('Проблема с сетевым соединением');
} else {
console.error('Ошибка API:', error.message);
}
// Пробрасываем ошибку дальше
throw error;
}
}
Продвинутые техники: загрузка файлов и прерывание запросов
Fetch API предоставляет мощные инструменты для реализации сложных сценариев взаимодействия с сервером. Рассмотрим продвинутые техники, которые повысят эффективность вашего кода. 🚀
Загрузка файлов на сервер с помощью FormData:
// Загрузка одного файла
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Ошибка загрузки: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Проблема при загрузке файла:', error);
throw error;
}
}
// Использование с input[type="file"]
document.querySelector('#fileInput').addEventListener('change', async (event) => {
const file = event.target.files[0];
if (file) {
try {
const result = await uploadFile(file);
console.log('Файл успешно загружен:', result);
} catch (error) {
console.error('Ошибка:', error);
}
}
});
Загрузка множества файлов с отслеживанием прогресса:
async function uploadFilesWithProgress(files, progressCallback) {
const formData = new FormData();
// Добавляем все файлы в FormData
Array.from(files).forEach((file, index) => {
formData.append(`file${index}`, file);
});
// XMLHttpRequest используется для отслеживания прогресса
// (Fetch API пока не имеет встроенной поддержки отслеживания прогресса)
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
progressCallback(percentComplete.toFixed(2));
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const response = JSON.parse(xhr.responseText);
resolve(response);
} catch (e) {
resolve(xhr.responseText);
}
} else {
reject(new Error(`HTTP ошибка: ${xhr.status}`));
}
});
xhr.addEventListener('error', () => reject(new Error('Сетевая ошибка')));
xhr.addEventListener('abort', () => reject(new Error('Загрузка прервана')));
xhr.open('POST', '/api/upload-multiple');
xhr.send(formData);
});
}
// Использование
document.querySelector('#multiFileInput').addEventListener('change', async (event) => {
const files = event.target.files;
if (files.length > 0) {
try {
const result = await uploadFilesWithProgress(files, (percent) => {
document.querySelector('#progressBar').value = percent;
document.querySelector('#progressText').textContent = `${percent}%`;
});
console.log('Файлы успешно загружены:', result);
} catch (error) {
console.error('Ошибка:', error);
}
}
});
Прерывание запросов с помощью AbortController:
// Создаём функцию для прерываемого запроса
function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
// Создаём контроллер прерывания
const controller = new AbortController();
const { signal } = controller;
// Создаём таймер, который прервёт запрос по истечении timeoutMs
const timeout = setTimeout(() => controller.abort(), timeoutMs);
// Добавляем signal к опциям запроса
return fetch(url, { ...options, signal })
.then(response => {
// Очищаем таймер при успешном ответе
clearTimeout(timeout);
return response;
})
.catch(error => {
// Очищаем таймер при ошибке
clearTimeout(timeout);
// Проверяем, была ли ошибка вызвана превышением времени ожидания
if (error.name === 'AbortError') {
throw new Error(`Запрос превысил время ожидания (${timeoutMs}ms)`);
}
throw error;
});
}
// Пример использования с возможностью ручного прерывания
let activeController = null;
function startSearch(query) {
// Прерываем предыдущий поиск, если он активен
if (activeController) {
activeController.abort();
}
// Создаём новый контроллер
activeController = new AbortController();
// Индикатор загрузки
document.querySelector('#searchStatus').textContent = 'Поиск...';
fetch(`/api/search?q=${query}`, {
signal: activeController.signal
})
.then(response => response.json())
.then(results => {
document.querySelector('#searchStatus').textContent = 'Поиск завершен';
displayResults(results);
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Поиск был прерван');
} else {
document.querySelector('#searchStatus').textContent = 'Ошибка поиска';
console.error('Ошибка поиска:', error);
}
})
.finally(() => {
if (activeController.signal.aborted) {
activeController = null;
}
});
}
// Кнопка прерывания поиска
document.querySelector('#cancelSearch').addEventListener('click', () => {
if (activeController) {
activeController.abort();
document.querySelector('#searchStatus').textContent = 'Поиск отменен';
}
});
Параллельные запросы с помощью Promise.all и Promise.allSettled:
// Выполнение нескольких запросов параллельно
async function fetchMultipleData() {
try {
const [users, products, settings] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/products').then(r => r.json()),
fetch('/api/settings').then(r => r.json())
]);
return { users, products, settings };
} catch (error) {
// Если любой из запросов завершится с ошибкой, выполнение прервётся
console.error('Один из запросов завершился с ошибкой:', error);
throw error;
}
}
// Параллельные запросы с независимой обработкой ошибок
async function fetchMultipleDataSafely() {
const results = await Promise.allSettled([
fetch('/api/users').then(r => r.json()),
fetch('/api/products').then(r => r.json()),
fetch('/api/settings').then(r => r.json())
]);
// Обработка результатов с учетом статуса каждого промиса
return results.map((result, index) => {
const endpointNames = ['users', 'products', 'settings'];
if (result.status === 'fulfilled') {
return { name: endpointNames[index], data: result.value, success: true };
} else {
console.error(`Ошибка при загрузке ${endpointNames[index]}:`, result.reason);
return { name: endpointNames[index], error: result.reason, success: false };
}
});
}
Повторные попытки запросов при временных ошибках:
async function fetchWithRetry(url, options = {}, maxRetries = 3, delayMs = 1000) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
// Увеличиваем задержку с каждой попыткой (экспоненциальная задержка)
if (attempt > 0) {
const backoffDelay = delayMs * Math.pow(2, attempt – 1);
console.log(`Повторная попытка ${attempt} через ${backoffDelay}ms...`);
await new Promise(resolve => setTimeout(resolve, backoffDelay));
}
const response = await fetch(url, options);
if (!response.ok) {
// Для определенных HTTP-статусов повторяем запрос
if ([429, 500, 502, 503, 504].includes(response.status)) {
const error = new Error(`HTTP ошибка ${response.status}`);
error.status = response.status;
throw error;
}
// Для других ошибок прекращаем попытки
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP ошибка ${response.status}`);
}
return response;
} catch (error) {
lastError = error;
// Не повторяем попытки для определенных ошибок
if (
(error.name === 'AbortError') ||
(error.status && ![429, 500, 502, 503, 504].includes(error.status))
) {
throw error;
}
// Если это последняя попытка, пробрасываем ошибку
if (attempt === maxRetries – 1) {
console.error(`Достигнуто максимальное количество попыток (${maxRetries})`);
throw lastError;
}
}
}
}
Fetch API кардинально изменил подход к асинхронным запросам в веб-разработке. Промисы и возможность использования async/await сделали код более читаемым и управляемым. Не бойтесь переходить на современные стандарты — они не просто следуют моде, но и решают реальные проблемы разработчиков. Независимо от того, создаёте ли вы простое приложение или сложную систему, Fetch API предлагает элегантное решение для всех типов HTTP-взаимодействий. Даже с появлением более новых библиотек и инструментов, понимание фундаментальных принципов Fetch API остаётся необходимым навыком, который позволяет гибко и эффективно работать с серверными данными в любых условиях.
Читайте также
Тимур Голубев
веб-разработчик