Пользователь печатает в поисковой строке "ноутбук" — каждый символ запускает новый fetch. К моменту когда он допечатал, летит 7 запросов. Ответы приходят не по порядку — результаты мелькают и показываются не те. Решение: AbortController — отменять предыдущий запрос при каждом новом вводе.
Контроль над уже запущенными операциями. Без AbortController завершить fetch после его отправки невозможно — можно только игнорировать ответ, но запрос всё равно потребляет ресурсы.
const controller = new AbortController()
const signal = controller.signal // объект AbortSignal
// Передаём signal в fetch
fetch('/api/search?q=ноутбук', { signal })
// Отменяем запрос
controller.abort()
// fetch выбросит DOMException с именем 'AbortError'async function search(query) {
const controller = new AbortController()
try {
const response = await fetch(`/api/search?q=${query}`, {
signal: controller.signal,
})
const data = await response.json()
return data
} catch (err) {
if (err.name === 'AbortError') {
console.log('Запрос отменён')
return null // отмена — это не ошибка
}
throw err // реальная сетевая ошибка — пробрасываем
}
}const controller = new AbortController()
const { signal } = controller
console.log(signal.aborted) // false
// Слушаем событие отмены
signal.addEventListener('abort', () => {
console.log('Запрос был отменён:', signal.reason)
})
controller.abort('Пользователь ушёл со страницы')
console.log(signal.aborted) // true
console.log(signal.reason) // 'Пользователь ушёл со страницы'В современных браузерах есть удобный статический метод:
// Автоматически отменяет запрос через 5 секунд
const response = await fetch('/api/data', {
signal: AbortSignal.timeout(5000),
})Один AbortController может управлять несколькими запросами:
const controller = new AbortController()
const { signal } = controller
// Запускаем несколько запросов параллельно
const [users, products, orders] = await Promise.all([
fetch('/api/users', { signal }),
fetch('/api/products', { signal }),
fetch('/api/orders', { signal }),
])
// Отменяем все сразу
controller.abort()При каждом новом символе в поле поиска — отменяем предыдущий запрос:
let searchController = null
async function handleSearchInput(query) {
// Отменяем предыдущий запрос, если он ещё идёт
if (searchController) {
searchController.abort()
}
// Создаём новый контроллер для нового запроса
searchController = new AbortController()
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: searchController.signal,
})
if (!response.ok) throw new Error('Ошибка сети')
const results = await response.json()
renderResults(results)
} catch (err) {
if (err.name !== 'AbortError') {
showError(err.message)
}
// AbortError молча игнорируем
}
}
// Подключаем к полю ввода
searchInput.addEventListener('input', (e) => {
handleSearchInput(e.target.value)
})AbortController можно использовать не только с fetch, но и с любым async-кодом:
async function longOperation(signal) {
for (let i = 0; i < 1000; i++) {
if (signal.aborted) throw new DOMException('Отменено', 'AbortError')
await processChunk(i)
}
}
const controller = new AbortController()
setTimeout(() => controller.abort(), 3000)
try {
await longOperation(controller.signal)
} catch (err) {
if (err.name === 'AbortError') console.log('Операция отменена по таймауту')
}Ошибка 1: не создают новый контроллер для каждого запроса
// Сломано: один контроллер использован повторно после abort()
const controller = new AbortController()
controller.abort()
// Второй запрос отменится мгновенно — signal уже aborted!
await fetch('/api/data', { signal: controller.signal })
// Исправлено: новый контроллер для каждого запроса
const controller = new AbortController()
// ...использовали, отменили
const newController = new AbortController() // новый для следующего запросаОшибка 2: не очищают таймер при отмене
// Сломано: таймер продолжает работать даже после успешного ответа
async function fetchWithTimeout(url, ms) {
const controller = new AbortController()
setTimeout(() => controller.abort(), ms) // таймер не очищается!
return fetch(url, { signal: controller.signal })
}
// Исправлено:
async function fetchWithTimeout(url, ms) {
const controller = new AbortController()
const timerId = setTimeout(() => controller.abort(), ms)
try {
const res = await fetch(url, { signal: controller.signal })
clearTimeout(timerId) // успех — очищаем таймер
return res
} catch (err) {
clearTimeout(timerId)
throw err
}
}Ошибка 3: AbortError обрабатывают как обычную ошибку
// Сломано: пользователь видит "Ошибка: Отменено" при каждом новом вводе
try {
const data = await fetch(url, { signal })
} catch (err) {
showError(err.message) // плохо — AbortError не ошибка, а нормальный сценарий
}
// Исправлено:
try {
const data = await fetch(url, { signal })
showResults(await data.json())
} catch (err) {
if (err.name === 'AbortError') return // молча игнорируем — это ожидаемо
showError(err.message) // только реальные ошибки
}Симуляция поиска с автоотменой предыдущего запроса при каждом новом вводе
// Симулируем fetch с задержкой и поддержкой AbortSignal
function fakeFetch(query, signal) {
return new Promise((resolve, reject) => {
// Если уже отменён до старта
if (signal.aborted) {
return reject(new DOMException('Отменено до старта', 'AbortError'))
}
const delay = 500 + Math.random() * 300 // 500-800мс задержка
const timerId = setTimeout(() => {
// К моменту ответа запрос мог быть отменён
if (signal.aborted) {
reject(new DOMException('Отменено во время выполнения', 'AbortError'))
} else {
resolve({
query,
results: [
`${query} — результат 1`,
`${query} — результат 2`,
`${query} — результат 3`,
],
})
}
}, delay)
// Если signal сработает — отменяем таймер
signal.addEventListener('abort', () => {
clearTimeout(timerId)
reject(new DOMException('Запрос отменён', 'AbortError'))
})
})
}
// Менеджер поиска с автоотменой
class SearchManager {
constructor() {
this.controller = null
this.totalRequests = 0
this.cancelledRequests = 0
this.completedRequests = 0
}
async search(query) {
// Отменяем предыдущий запрос
if (this.controller) {
this.controller.abort()
this.cancelledRequests++
}
this.controller = new AbortController()
this.totalRequests++
const requestId = this.totalRequests
console.log(`[#${requestId}] Запрос: "${query}"`)
try {
const data = await fakeFetch(query, this.controller.signal)
this.completedRequests++
console.log(`[#${requestId}] Результат для "${query}":"`, data.results)
return data
} catch (err) {
if (err.name === 'AbortError') {
console.log(`[#${requestId}] Отменён (новый запрос пришёл раньше)`)
return null
}
throw err
}
}
getStats() {
return {
total: this.totalRequests,
cancelled: this.cancelledRequests,
completed: this.completedRequests,
}
}
}
// Симуляция быстрого ввода пользователя
async function runDemo() {
const manager = new SearchManager()
// Пользователь быстро набирает "ноутбук"
const queries = ['н', 'но', 'ноу', 'ноут', 'ноутб', 'ноутбу', 'ноутбук']
for (const q of queries) {
manager.search(q) // не await — запускаем быстро, не ждём
await new Promise(r => setTimeout(r, 150)) // 150мс между символами
}
// Ждём завершения последнего запроса
await new Promise(r => setTimeout(r, 1000))
console.log('\nСтатистика:')
const stats = manager.getStats()
console.log('Всего запросов:', stats.total)
console.log('Отменено:', stats.cancelled)
console.log('Завершено:', stats.completed)
}
runDemo()Пользователь печатает в поисковой строке "ноутбук" — каждый символ запускает новый fetch. К моменту когда он допечатал, летит 7 запросов. Ответы приходят не по порядку — результаты мелькают и показываются не те. Решение: AbortController — отменять предыдущий запрос при каждом новом вводе.
Контроль над уже запущенными операциями. Без AbortController завершить fetch после его отправки невозможно — можно только игнорировать ответ, но запрос всё равно потребляет ресурсы.
const controller = new AbortController()
const signal = controller.signal // объект AbortSignal
// Передаём signal в fetch
fetch('/api/search?q=ноутбук', { signal })
// Отменяем запрос
controller.abort()
// fetch выбросит DOMException с именем 'AbortError'async function search(query) {
const controller = new AbortController()
try {
const response = await fetch(`/api/search?q=${query}`, {
signal: controller.signal,
})
const data = await response.json()
return data
} catch (err) {
if (err.name === 'AbortError') {
console.log('Запрос отменён')
return null // отмена — это не ошибка
}
throw err // реальная сетевая ошибка — пробрасываем
}
}const controller = new AbortController()
const { signal } = controller
console.log(signal.aborted) // false
// Слушаем событие отмены
signal.addEventListener('abort', () => {
console.log('Запрос был отменён:', signal.reason)
})
controller.abort('Пользователь ушёл со страницы')
console.log(signal.aborted) // true
console.log(signal.reason) // 'Пользователь ушёл со страницы'В современных браузерах есть удобный статический метод:
// Автоматически отменяет запрос через 5 секунд
const response = await fetch('/api/data', {
signal: AbortSignal.timeout(5000),
})Один AbortController может управлять несколькими запросами:
const controller = new AbortController()
const { signal } = controller
// Запускаем несколько запросов параллельно
const [users, products, orders] = await Promise.all([
fetch('/api/users', { signal }),
fetch('/api/products', { signal }),
fetch('/api/orders', { signal }),
])
// Отменяем все сразу
controller.abort()При каждом новом символе в поле поиска — отменяем предыдущий запрос:
let searchController = null
async function handleSearchInput(query) {
// Отменяем предыдущий запрос, если он ещё идёт
if (searchController) {
searchController.abort()
}
// Создаём новый контроллер для нового запроса
searchController = new AbortController()
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: searchController.signal,
})
if (!response.ok) throw new Error('Ошибка сети')
const results = await response.json()
renderResults(results)
} catch (err) {
if (err.name !== 'AbortError') {
showError(err.message)
}
// AbortError молча игнорируем
}
}
// Подключаем к полю ввода
searchInput.addEventListener('input', (e) => {
handleSearchInput(e.target.value)
})AbortController можно использовать не только с fetch, но и с любым async-кодом:
async function longOperation(signal) {
for (let i = 0; i < 1000; i++) {
if (signal.aborted) throw new DOMException('Отменено', 'AbortError')
await processChunk(i)
}
}
const controller = new AbortController()
setTimeout(() => controller.abort(), 3000)
try {
await longOperation(controller.signal)
} catch (err) {
if (err.name === 'AbortError') console.log('Операция отменена по таймауту')
}Ошибка 1: не создают новый контроллер для каждого запроса
// Сломано: один контроллер использован повторно после abort()
const controller = new AbortController()
controller.abort()
// Второй запрос отменится мгновенно — signal уже aborted!
await fetch('/api/data', { signal: controller.signal })
// Исправлено: новый контроллер для каждого запроса
const controller = new AbortController()
// ...использовали, отменили
const newController = new AbortController() // новый для следующего запросаОшибка 2: не очищают таймер при отмене
// Сломано: таймер продолжает работать даже после успешного ответа
async function fetchWithTimeout(url, ms) {
const controller = new AbortController()
setTimeout(() => controller.abort(), ms) // таймер не очищается!
return fetch(url, { signal: controller.signal })
}
// Исправлено:
async function fetchWithTimeout(url, ms) {
const controller = new AbortController()
const timerId = setTimeout(() => controller.abort(), ms)
try {
const res = await fetch(url, { signal: controller.signal })
clearTimeout(timerId) // успех — очищаем таймер
return res
} catch (err) {
clearTimeout(timerId)
throw err
}
}Ошибка 3: AbortError обрабатывают как обычную ошибку
// Сломано: пользователь видит "Ошибка: Отменено" при каждом новом вводе
try {
const data = await fetch(url, { signal })
} catch (err) {
showError(err.message) // плохо — AbortError не ошибка, а нормальный сценарий
}
// Исправлено:
try {
const data = await fetch(url, { signal })
showResults(await data.json())
} catch (err) {
if (err.name === 'AbortError') return // молча игнорируем — это ожидаемо
showError(err.message) // только реальные ошибки
}Симуляция поиска с автоотменой предыдущего запроса при каждом новом вводе
// Симулируем fetch с задержкой и поддержкой AbortSignal
function fakeFetch(query, signal) {
return new Promise((resolve, reject) => {
// Если уже отменён до старта
if (signal.aborted) {
return reject(new DOMException('Отменено до старта', 'AbortError'))
}
const delay = 500 + Math.random() * 300 // 500-800мс задержка
const timerId = setTimeout(() => {
// К моменту ответа запрос мог быть отменён
if (signal.aborted) {
reject(new DOMException('Отменено во время выполнения', 'AbortError'))
} else {
resolve({
query,
results: [
`${query} — результат 1`,
`${query} — результат 2`,
`${query} — результат 3`,
],
})
}
}, delay)
// Если signal сработает — отменяем таймер
signal.addEventListener('abort', () => {
clearTimeout(timerId)
reject(new DOMException('Запрос отменён', 'AbortError'))
})
})
}
// Менеджер поиска с автоотменой
class SearchManager {
constructor() {
this.controller = null
this.totalRequests = 0
this.cancelledRequests = 0
this.completedRequests = 0
}
async search(query) {
// Отменяем предыдущий запрос
if (this.controller) {
this.controller.abort()
this.cancelledRequests++
}
this.controller = new AbortController()
this.totalRequests++
const requestId = this.totalRequests
console.log(`[#${requestId}] Запрос: "${query}"`)
try {
const data = await fakeFetch(query, this.controller.signal)
this.completedRequests++
console.log(`[#${requestId}] Результат для "${query}":"`, data.results)
return data
} catch (err) {
if (err.name === 'AbortError') {
console.log(`[#${requestId}] Отменён (новый запрос пришёл раньше)`)
return null
}
throw err
}
}
getStats() {
return {
total: this.totalRequests,
cancelled: this.cancelledRequests,
completed: this.completedRequests,
}
}
}
// Симуляция быстрого ввода пользователя
async function runDemo() {
const manager = new SearchManager()
// Пользователь быстро набирает "ноутбук"
const queries = ['н', 'но', 'ноу', 'ноут', 'ноутб', 'ноутбу', 'ноутбук']
for (const q of queries) {
manager.search(q) // не await — запускаем быстро, не ждём
await new Promise(r => setTimeout(r, 150)) // 150мс между символами
}
// Ждём завершения последнего запроса
await new Promise(r => setTimeout(r, 1000))
console.log('\nСтатистика:')
const stats = manager.getStats()
console.log('Всего запросов:', stats.total)
console.log('Отменено:', stats.cancelled)
console.log('Завершено:', stats.completed)
}
runDemo()Напиши функцию fetchWithTimeout(url, ms), которая выполняет fetch-запрос и автоматически отменяет его, если он не завершится за ms миллисекунд. При таймауте функция должна выбрасывать ошибку с сообщением "Таймаут: запрос превысил Xмс".
const controller = new AbortController(); const timerId = setTimeout(() => controller.abort(), ms); return fetch(url, { signal: controller.signal })