AbortController w JavaScript pozwala przerwać operację asynchroniczną obsługującą AbortSignal, a jednym z jego najważniejszych zastosowań jest anulowanie żądania wykonywanego przez fetch(). Przydaje się wtedy, gdy użytkownik opuszcza widok przed zakończeniem pobierania danych, rozpoczyna nowe wyszukiwanie zanim poprzednie żądanie otrzyma odpowiedź albo aplikacja nie chce czekać na serwer dłużej niż określony czas. Sam Promise nie posiada uniwersalnej funkcji anulowania, dlatego mechanizm opiera się na przekazaniu do operacji sygnału i późniejszej zmianie jego stanu przez AbortController. Poprawne użycie wymaga jednak zrozumienia różnicy między kontrolerem i sygnałem, obsługi błędu przerwania oraz faktu, że raz anulowany AbortSignal nie może wrócić do stanu aktywnego.
Co to jest AbortController?
AbortController jest interfejsem Web API służącym do wysyłania informacji, że wykonywana operacja powinna zostać przerwana. Sam kontroler nie zatrzymuje dowolnego kodu JavaScript. Działa z operacjami, które potrafią odebrać przekazany im AbortSignal i odpowiednio zareagować na zmianę jego stanu.
Najbardziej typowym przykładem jest `fetch()`. Bez mechanizmu anulowania aplikacja może rozpocząć pobieranie danych, których po chwili już nie potrzebuje. Sytuacja jest częsta w interfejsach dynamicznych - użytkownik szybko zmienia filtr, przechodzi na inną podstronę albo zamyka widok przed otrzymaniem odpowiedzi serwera.
Podstawowy schemat składa się z trzech kroków:
const controller = new AbortController();
fetch("/api/products", {
signal: controller.signal
});
controller.abort();
Najpierw tworzony jest kontroler. Następnie jego `signal` zostaje przekazany do `fetch()`. Gdy aplikacja wywoła `controller.abort()`, sygnał zmienia stan na przerwany, a operacja korzystająca z tego sygnału może zakończyć dalsze działanie.
AbortController a AbortSignal - czym się różnią?
AbortController i AbortSignal tworzą jeden mechanizm, ale pełnią różne role. Kontroler inicjuje przerwanie, natomiast sygnał przekazuje informację o przerwaniu do operacji.
| Element | Zadanie |
|---|---|
| AbortController | Tworzy sygnał i udostępnia metodę abort() |
| AbortSignal | Przekazuje informację, że operacja została anulowana |
| controller.signal | Zwraca sygnał powiązany z konkretnym kontrolerem |
| controller.abort() | Zmienia stan sygnału na przerwany |
Takie rozdzielenie ma praktyczną zaletę. Kod wykonujący operację nie musi otrzymywać całego kontrolera i możliwości samodzielnego jej anulowania. Można przekazać mu wyłącznie sygnał:
async function loadProducts(signal) {
const response = await fetch("/api/products", {
signal
});
return response.json();
}
const controller = new AbortController();
loadProducts(controller.signal);
Część odpowiedzialna za pobieranie otrzymuje więc informację o ewentualnym przerwaniu, ale decyzja o wywołaniu `abort()` pozostaje w kodzie zarządzającym cyklem życia operacji.
Jak przerwać fetch za pomocą AbortController?
W praktycznej aplikacji wywołanie `abort()` jest zwykle związane z konkretnym zdarzeniem - kliknięciem przycisku, przejściem do innego widoku albo rozpoczęciem nowego żądania.
const controller = new AbortController();
async function loadData() {
try {
const response = await fetch("/api/data", {
signal: controller.signal
});
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
loadData();
document
.querySelector("#cancel")
.addEventListener("click", () => {
controller.abort();
});
Jeżeli użytkownik kliknie przycisk przed zakończeniem operacji, żądanie zostanie przerwane. Nie oznacza to jednak cofnięcia czynności, które serwer zdążył już wykonać. AbortController pozwala przerwać oczekiwanie i dalszą obsługę żądania po stronie klienta, ale nie jest mechanizmem transakcyjnym anulującym skutki wykonane wcześniej przez system backendowy.
Ma to szczególne znaczenie przy żądaniach modyfikujących dane. Jeśli serwer otrzymał i przetworzył operację przed anulowaniem po stronie przeglądarki, samo `abort()` nie przywróci wcześniejszego stanu bazy danych.
Co dzieje się po wywołaniu abort()?
Po wywołaniu `controller.abort()` właściwość `controller.signal.aborted` przyjmuje wartość `true`. Sygnał pozostaje od tego momentu w stanie przerwanym.
const controller = new AbortController();
console.log(controller.signal.aborted);
// false
controller.abort();
console.log(controller.signal.aborted);
// true
W przypadku `fetch()` anulowanie może spowodować odrzucenie Promise. Dlatego kod korzystający z `await` powinien uwzględniać obsługę takiego zakończenia.
Przerwanie może dotyczyć nie tylko okresu oczekiwania na początkową odpowiedź. Jeśli pobieranie odpowiedzi nadal trwa, sygnał może również zakończyć dalszą pracę z transmisją obsługiwaną przez fetch.
Jak obsłużyć AbortError?
Anulowanie wykonane przez aplikację jest sytuacją oczekiwaną i zazwyczaj nie powinno być traktowane tak samo jak błąd serwera czy utrata połączenia z internetem.
Dlatego zamiast wyświetlać każdy przechwycony wyjątek użytkownikowi, warto rozróżnić przerwanie kontrolowane od rzeczywistego problemu:
async function loadData(signal) {
try {
const response = await fetch("/api/data", {
signal
});
return await response.json();
} catch (error) {
if (error.name === "AbortError") {
console.log("Pobieranie zostało anulowane");
return;
}
throw error;
}
}
Ma to znaczenie również dla systemów monitoringu błędów. Celowe przerwanie requestu po zmianie widoku nie powinno być raportowane jako awaria aplikacji, jeżeli jest prawidłowym elementem jej działania.
Live search - dlaczego warto anulować poprzednie żądanie?
Jednym z najlepszych przykładów zastosowania AbortController jest wyszukiwarka wysyłająca zapytanie do API po zmianie wpisanego tekstu.
Użytkownik może wpisać najpierw:
"lap"
i chwilę później:
"laptop"
Aplikacja wysyła dwa żądania:
request A: "lap" → request B: "laptop"
Nie ma jednak gwarancji, że odpowiedzi wrócą w identycznej kolejności. Serwer może szybciej obsłużyć drugie żądanie. Wtedy aplikacja najpierw pokaże prawidłowe wyniki dla "laptop", a po chwili wolniejsza odpowiedź dla "lap" może nadpisać aktualny widok starszymi wynikami.
Jednym ze sposobów ograniczenia takiego problemu jest anulowanie poprzedniego requestu przed rozpoczęciem następnego:
let controller;
async function searchProducts(query) {
if (controller) {
controller.abort();
}
controller = new AbortController();
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(query)}`,
{
signal: controller.signal
}
);
const products = await response.json();
renderProducts(products);
} catch (error) {
if (error.name !== "AbortError") {
console.error(error);
}
}
}
Każde nowe wyszukiwanie anuluje poprzednią operację i tworzy własny kontroler. Dzięki temu kod nie musi przetwarzać wyników requestu, który z punktu widzenia interfejsu stracił już znaczenie.
W praktycznej wyszukiwarce AbortController warto często połączyć z debounce. Debounce zmniejsza liczbę wysyłanych zapytań, natomiast anulowanie pomaga obsłużyć te żądania, które zostały już uruchomione, ale przestały być aktualne.
Dlaczego trzeba tworzyć nowy AbortController?
Częstym błędem jest próba wielokrotnego wykorzystywania tego samego kontrolera po wywołaniu `abort()`.
const controller = new AbortController();
controller.abort();
fetch("/api/data", {
signal: controller.signal
});
Sygnał powiązany z kontrolerem jest już przerwany. `abort()` nie działa jak przycisk, który można przełączać pomiędzy stanem "stop" i "start". AbortSignal jest jednokrotnego użytku w tym sensie, że po anulowaniu pozostaje anulowany.
Jeżeli aplikacja rozpoczyna nową niezależną operację, powinna utworzyć nowy kontroler:
let controller = new AbortController();
// pierwsza operacja
controller.abort();
// nowa operacja
controller = new AbortController();
fetch("/api/data", {
signal: controller.signal
});
Ten szczegół ma duże znaczenie w komponentach i funkcjach wyszukujących, gdzie kontrolery są często zastępowane przy każdym kolejnym żądaniu.
Jak ustawić timeout dla fetch?
`fetch()` nie wymaga sam w sobie podania klasycznego parametru w rodzaju `timeout: 5000`. AbortController można jednak wykorzystać do przerwania operacji po określonym czasie.
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, 5000);
try {
const response = await fetch("/api/report", {
signal: controller.signal
});
const data = await response.json();
clearTimeout(timeoutId);
console.log(data);
} catch (error) {
clearTimeout(timeoutId);
if (error.name === "AbortError") {
console.log("Przekroczono czas oczekiwania");
} else {
throw error;
}
}
W tym przykładzie aplikacja czeka maksymalnie około pięciu sekund, a następnie wysyła sygnał przerwania. Ważne jest usunięcie timera również po wcześniejszym zakończeniu operacji, aby nie pozostawiać niepotrzebnego zadania zaplanowanego w środowisku.
Trzeba też pamiętać, że "timeout" jest tutaj polityką po stronie klienta. Nie oznacza automatycznie, że serwer po pięciu sekundach przerwie wykonywanie rozpoczętego wcześniej zadania.
AbortSignal.timeout() - prostszy timeout
W nowoczesnych środowiskach można skorzystać również ze statycznej metody `AbortSignal.timeout()`. Tworzy ona sygnał, który zostanie automatycznie anulowany po określonym czasie.
try {
const response = await fetch("/api/report", {
signal: AbortSignal.timeout(5000)
});
const data = await response.json();
console.log(data);
} catch (error) {
if (error.name === "TimeoutError") {
console.log("Serwer nie odpowiedział wystarczająco szybko");
} else {
throw error;
}
}
Kod jest krótszy, ponieważ nie wymaga tworzenia własnego `setTimeout()` i ręcznego wywoływania `abort()`. Trzeba jednak uwzględnić wymagania projektu dotyczące obsługiwanych wersji przeglądarek. W starszych środowiskach prosty wariant z AbortController i timerem może być bardziej przewidywalny.
Warto też odróżnić celowe anulowanie od przekroczenia czasu. W przypadku sygnału tworzonego przez `AbortSignal.timeout()` operacja może zakończyć się błędem `TimeoutError`, co pozwala aplikacji inaczej poinformować użytkownika o zbyt długim oczekiwaniu.
Czy jednym AbortController można przerwać kilka żądań?
Tak. Ten sam `AbortSignal` można przekazać do kilku operacji. Wywołanie `abort()` spowoduje wtedy zmianę stanu wspólnego sygnału, dzięki czemu wszystkie korzystające z niego operacje obsługujące anulowanie mogą zostać przerwane.
const controller = new AbortController();
const requests = [
fetch("/api/user", {
signal: controller.signal
}),
fetch("/api/orders", {
signal: controller.signal
}),
fetch("/api/messages", {
signal: controller.signal
})
];
controller.abort();
Takie rozwiązanie jest użyteczne, gdy kilka requestów należy do jednego widoku. Jeżeli użytkownik opuszcza ekran, można anulować cały zestaw operacji jednym kontrolerem.
Nie należy jednak łączyć jednym kontrolerem operacji o całkowicie niezależnych cyklach życia. W przeciwnym razie anulowanie jednej czynności może przypadkowo zakończyć request, który nadal jest potrzebny.
AbortSignal nie tylko dla fetch
Mechanizm AbortSignal nie jest ograniczony wyłącznie do pobierania danych. Sygnał może być wykorzystywany także przez inne API, które przewidują obsługę anulowania.
Ciekawym przykładem jest `addEventListener()`. Sygnał można przekazać w opcjach listenera:
const controller = new AbortController();
window.addEventListener(
"resize",
handleResize,
{
signal: controller.signal
}
);
controller.abort();
Po anulowaniu sygnału listener powiązany w ten sposób może zostać usunięty bez osobnego wywoływania `removeEventListener()`. Jest to szczególnie przydatne, gdy komponent rejestruje kilka zdarzeń należących do jednego cyklu życia.
const controller = new AbortController();
document.addEventListener(
"click",
handleClick,
{ signal: controller.signal }
);
window.addEventListener(
"resize",
handleResize,
{ signal: controller.signal }
);
window.addEventListener(
"scroll",
handleScroll,
{ signal: controller.signal }
);
// sprzątanie całej grupy
controller.abort();
Takie podejście może uprościć kod odpowiedzialny za sprzątanie zasobów, pod warunkiem że wszystkie elementy rzeczywiście mają wspólny czas życia.
Najczęstsze błędy przy używaniu AbortController
Ponowne używanie anulowanego sygnału
Po wywołaniu `abort()` sygnał pozostaje przerwany. Dla nowej operacji należy utworzyć nowy AbortController.
Traktowanie AbortError jak awarii aplikacji
Jeżeli anulowanie jest częścią zaprojektowanego działania interfejsu, nie powinno generować takiego samego komunikatu jak błąd sieci lub odpowiedź serwera 500.
Brak powiązania kontrolera z cyklem życia
Kontroler powinien mieć właściciela. Jeżeli jest tworzony podczas uruchamiania widoku, warto jasno określić, w którym miejscu następuje jego anulowanie lub zastąpienie.
Przekonanie, że abort cofa operację serwera
Przerwanie po stronie klienta nie jest mechanizmem rollback. Backend mógł już otrzymać żądanie i wykonać zmianę.
Anulowanie zamiast rozwiązania problemu race condition
AbortController może być bardzo użyteczny przy nieaktualnych requestach, ale logika aplikacji nadal powinna być odporna na asynchroniczność. W bardziej złożonych przypadkach stosuje się również identyfikatory żądań, wersjonowanie danych albo sprawdzanie, czy odpowiedź nadal odpowiada aktualnemu stanowi interfejsu.
Kiedy warto stosować AbortController?
AbortController daje największą wartość wtedy, gdy aplikacja potrafi jednoznacznie stwierdzić, że wynik rozpoczętej wcześniej operacji nie będzie już potrzebny.
| Sytuacja | Czy anulowanie ma sens? |
|---|---|
| Użytkownik rozpoczyna nowe wyszukiwanie | Tak - poprzedni wynik może być już nieaktualny |
| Użytkownik opuszcza widok w SPA | Tak - dane dla zamkniętego widoku mogą nie być potrzebne |
| Pobieranie trwa dłużej niż dopuszcza aplikacja | Tak - można zastosować timeout |
| Kilka requestów należy do jednego komponentu | Tak - można wykorzystać wspólny sygnał |
| Serwer wykonuje już ważną operację zapisu | Abort przerwie oczekiwanie klienta, ale nie gwarantuje cofnięcia operacji |
Najważniejsze jest więc traktowanie AbortController nie jako dodatku do `fetch()`, lecz jako narzędzia do zarządzania cyklem życia operacji asynchronicznych. Jeśli użytkownik zmienił widok, rozpoczął nowe wyszukiwanie albo aplikacja przekroczyła dopuszczalny czas oczekiwania, dalsze wykonywanie starej operacji często nie ma już wartości. Przekazanie AbortSignal pozwala tę decyzję wyrazić bez budowania własnego systemu flag i ręcznego sprawdzania stanu w wielu miejscach kodu.

Komentarze