Quick Tips

AbortController und AbortSignal in JavaScript

: Generalisiertes und vereinfachtes Abbrechen von Aufgaben

Um in JavaScript auf Events zu reagieren, kommt man man selten um die Funktion addEventListener herum. Hier gibt es seit einiger Zeit Abhilfe durch den AbortController und das AbortSignal.

Möchte man auf mehreren Elementen einen EventListener hinzufügen, gestaltet sich der Code in etwa so:

element1.addEventListener("click", () => console.log("element1 clicked"));
element2.addEventListener("click", () => console.log("element2 clicked"));
element3.addEventListener("click", () => console.log("element3 clicked"));

Möchte man die EventListener auch wieder entfernen, sind dafür einige Umstrukturierungen notwendig. Um einen Listener wieder zu entfernen, muss die Referenz auf die Handler-Funktion vorher in eine Variable ausgelagert werden:

const element1Handler = () => console.log("element1 clicked");
const element2Handler = () => console.log("element2 clicked");
const element3Handler = () => console.log("element3 clicked");

element1.addEventListener("click", element1Handler);
element2.addEventListener("click", element2Handler);
element3.addEventListener("click", element3Handler);

// ...

element1.removeEventListener("click", element1Handler);
element2.removeEventListener("click", element2Handler);
element3.removeEventListener("click", element3Handler);

Diese Vorgehensweise kann bei vielen Elementen schnell unübersichtlich und wartungsunfreundlich werden. Hier hilft der AbortController. Mit ihm können gleich EventListener von beliebig vielen Elementen entfernt werden. Über eine Instanz des AbortControllers kann dabei signallisiert werden, dass etwas ,,abgebrochen“ werden soll. Ein AbortController kommt immer mit einem AbortSignal. Das AbortSignal wird dabei an die Funktion oder API übergeben, die abgebrochen werden können soll.

„Abbrechen“ bedeutet hier allerdings auch, dass damit EventListener entfernt werden können. addEventListener hat einen dritten Parameter für weitere Optionen, worüber das Signal zum Abbrechen übergeben werden kann. Das obige Beispiel könnte so umgeschrieben werden:

const controller = new AbortController();

const signal = controller.signal;

element1.addEventListener("click", () => console.log("element1 clicked"), {
    signal,
});
element2.addEventListener("click", () => console.log("element2 clicked"), {
    signal,
});
element3.addEventListener("click", () => console.log("element2 clicked"), {
    signal,
});

// Remove all event listeners at once
controller.abort();

Dadurch, dass jedes Element kein removeEventListener mehr benötigt, entfällt zusätzlich die Notwendigkeit, die Handler-Funktion in eine Variable auszulagern.

Hierzu noch ein weiterer Tipp: Soll nur ein einziges Mal auf ein Event reagiert werden, kann auch die once-Option bei addEventListener gesetzt werden. Dadurch wird der Handler nach einmaligem Aufruf entfernt und ein manuelles Entfernen des Listeners entfällt komplett:

element4.addEventListener("click", () => console.log("element4 clicked"), {
    once: true,
});

Mit dem AbortController lassen sich aber nicht nur EventListener entfernen. Auch das Abbrechen eines fetch() ist damit möglich. Dabei wird das Signal einfach als Option im fetch-Aufruf übergeben. Auf diese Art lässt sich ein Timeout-Mechanismus für fetch-Requests umsetzen, der die Anfrage tatsächlich abbricht. Wird der Request abgebrochen, wird das Promise des Requests mit einer DOMException rejected:

const controller = new AbortController();

setTimeout(() => controller.abort(), 10_000);

try {
    await fetch("https://developer.mozilla.org", {
        signal: controller.signal,
    });
} catch(e) {
    console.error(e);
}

Möchte man selbst einen Abbruch-Mechanismus für eine eigene Funktion implementieren, kann dies über das AbortSignal getan werden: Das Signal eines AbortControllers besitzt nicht nur die Eigenschaft aborted, die angibt, ob über den entsprechenden Controller ein Abbruch angefragt wurde. Zusätzlich besitzt das AbortSignal ein Event, das im Falle eines gewünschten Abbruchs ausgelöst wird. Auf diese Weise kann das promisified setTimeout aus einem vorherigen Quick-Tip um einen generaliserten Abbruch-Mechanismus erweitert werden:

function delay(duration = 0, signal = undefined) {
    return new Promise((resolve, reject) => {
        const timeoutId = setTimeout(resolve, duration);
        signal?.addEventListener(
            "abort",
            () => {
                clearTimeout(timeoutId);
                reject(new Error("delay aborted"));
            },
            { once: true },
        );
    });
}

// ...

async function myFunction() {
    console.log("Do something");
    const controller = new AbortController();
    
    // Simulate click on "abort" button
    setTimeout(() => controller.abort(), 500);

    await delay(10_000, controller.signal);
    console.log("Do something else");
}

Die Klassen AbortController und AbortSignal existieren schon länger und können in folgenden Browsern verwendet werden:

AbortController & AbortSignal auf caniuse.com
AbortController & AbortSignal auf caniuse.com

Niklas Mollenhauer

Full-Stack-Entwickler mit Leidenschaft für API-, DX- und Language-Design. Folge Niklas auf GitHub.