Start · Sprachen · JavaScript · Referenz · AbortSignal

AbortSignal

Interface

Das <code>AbortSignal</code>-Interface repräsentiert ein Signalobjekt, mit dem eine asynchrone Operation abgebrochen werden kann.

Kategorie: web-api

Signatur

interface AbortSignal extends EventTarget

Beschreibung

Das AbortSignal-Interface repräsentiert ein Signalobjekt, das es ermöglicht, mit einer asynchronen Operation (wie einer fetch-Anfrage) zu kommunizieren und diese bei Bedarf über ein AbortController-Objekt abzubrechen.

Instanz-Eigenschaften

Erbt außerdem Eigenschaften von seinem Eltern-Interface EventTarget.

  • AbortSignal.aborted (schreibgeschützt): Ein Boolean, der angibt, ob die Anfrage(n), mit denen das Signal kommuniziert, abgebrochen wurde(n) (true) oder nicht (false).
  • AbortSignal.reason (schreibgeschützt): Ein JavaScript-Wert, der den Abbruchgrund liefert, sobald das Signal abgebrochen wurde.

Statische Methoden

Erbt außerdem Methoden von seinem Eltern-Interface EventTarget.

  • AbortSignal.abort(): Gibt eine AbortSignal-Instanz zurück, die bereits als abgebrochen gesetzt ist.
  • AbortSignal.any(): Gibt ein AbortSignal zurück, das abbricht, sobald eines der übergebenen Abbruchsignale abbricht.
  • AbortSignal.timeout(): Gibt eine AbortSignal-Instanz zurück, die nach einer angegebenen Zeit automatisch abbricht.

Instanz-Methoden

Erbt außerdem Methoden von seinem Eltern-Interface EventTarget.

  • AbortSignal.throwIfAborted(): Wirft die reason des Signals, falls das Signal abgebrochen wurde; andernfalls tut es nichts.

Events

Erbt außerdem Events von seinem Eltern-Interface EventTarget.

Auf dieses Event kann mit addEventListener() gehört werden oder indem ein Event-Listener der oneventname-Eigenschaft dieses Interfaces zugewiesen wird.

  • abort: Wird ausgelöst, wenn die asynchronen Operationen, mit denen das Signal kommuniziert, abgebrochen werden. Auch über die onabort-Eigenschaft verfügbar.

Entfernen des abort-Event-Listeners

Signale, die von AbortController-Objekten erzeugt werden, können durch die Garbage Collection freigegeben werden, sobald sowohl das Signal als auch der zugehörige Controller nicht mehr erreichbar sind — sogar mit registrierten abort-Event-Listenern —, weil garantiert ist, dass das Event nicht ausgelöst wird. Signale, deren Abbruch nicht von einem Controller verwaltet wird, werden dagegen durch die Existenz eines abort-Event-Listeners am Leben gehalten:

  • Ein nicht abgebrochenes Signal, das von AbortSignal.any() zurückgegeben wird, wird am Leben gehalten, solange es noch Quellsignale und entweder angehängte abort-Listener oder von einer API registrierte interne Abbruchschritte hat.
  • Ein von AbortSignal.timeout() zurückgegebenes Signal wird am Leben gehalten, solange sein Timeout aussteht und es angehängte abort-Listener hat.

Die folgende Funktion kombiniert einen anwendungsweiten Abbruch mit einem vom Aufrufer für eine einzelne Operation gelieferten Signal. Sie fügt einen Listener hinzu, um den Abbruch zu protokollieren, verlässt sich aber auf { once: true }, um ihn zu entfernen.

{ once: true } entfernt den Listener nur, wenn das Event ausgelöst wird. Wenn keines der Eingangssignale abbricht, bleibt der Listener auch nach dem Lesen des Antwort-Bodys bestehen. Wiederholte Aufrufe können daher kombinierte Signale und ihre Listener so lange halten, wie das globale Signal erreichbar bleibt und die kombinierten Signale nicht abgebrochen wurden. Das Verwerfen des kombinierten Signals entfernt den Listener nicht, und fetch() räumt keine von Ihrem Code hinzugefügten Listener auf.

Stattdessen sollte der Listener entfernt werden, wenn die Operation abgeschlossen ist, unabhängig davon, ob sie erfolgreich war oder fehlgeschlagen ist. Verwenden Sie einen benannten Listener, damit Sie ihn in einem finally-Block entfernen können.

Das await auf response.text() stellt sicher, dass der Listener registriert bleibt, bis der Antwort-Body gelesen wurde. Diese Aufräumarbeit betrifft den vom Beispiel hinzugefügten Listener, nicht die interne Abbruchbehandlung von fetch(). Wenn eine Operation nur einen anwendungsweiten Abbruch benötigt, sollte globalController.signal direkt übergeben werden, statt ein kombiniertes Signal zu erzeugen.

Eine abbrechbare API implementieren

Eine API, die Abbrüche unterstützen soll, kann ein AbortSignal-Objekt akzeptieren und dessen Zustand nutzen, um die Behandlung des Abbruchsignals bei Bedarf auszulösen.

Eine auf Promise basierende API sollte auf das Abbruchsignal reagieren, indem sie ein noch nicht erledigtes Promise mit der reason des AbortSignal abweist. Betrachten Sie beispielsweise die folgende myCoolPromiseAPI, die ein Signal entgegennimmt und ein Promise zurückgibt. Das Promise wird sofort abgewiesen, wenn das Signal bereits abgebrochen ist oder wenn das Abort-Event erkannt wird. Andernfalls schließt es nach einer Verzögerung normal ab und löst das Promise auf.

Entfernen Sie den abort-Listener, wenn die Operation normal abgeschlossen ist, damit ein langlebiges Signal nicht den Listener und die von ihm referenzierten Werte behält. Auch hier entfernt { once: true } den Listener nur, wenn das Signal tatsächlich abbricht.

APIs, die keine Promises zurückgeben, können auf ähnliche Weise reagieren. In einigen Fällen kann es sinnvoll sein, das Signal aufzunehmen.

Beispiele

Eine fetch-Operation mit einem expliziten Signal abbrechen

let controller;
const url = "video.mp4";

const downloadBtn = document.querySelector(".download");
const abortBtn = document.querySelector(".abort");

downloadBtn.addEventListener("click", fetchVideo);

abortBtn.addEventListener("click", () => {
  if (controller) {
    controller.abort();
    console.log("Download aborted");
  }
});

async function fetchVideo() {
  controller = new AbortController();
  const signal = controller.signal;

  try {
    const response = await fetch(url, { signal });
    console.log("Download complete", response);
    // process response further
  } catch (err) {
    console.error(`Download error: ${err.message}`);
  }
}

Abbruch nach Lesen des Response-Bodys

async function get() {
  const controller = new AbortController();
  const request = new Request("https://example.org/get", {
    signal: controller.signal,
  });

  const response = await fetch(request);
  controller.abort();
  // The next line will throw `AbortError`
  const text = await response.text();
  console.log(text);
}

Eine fetch-Operation mit Timeout abbrechen

const url = "video.mp4";

try {
  const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
  const result = await res.blob();
  // …
} catch (err) {
  if (err.name === "TimeoutError") {
    console.error("Timeout: It took more than 5 seconds to get the result!");
  } else if (err.name === "AbortError") {
    console.error(
      "Fetch aborted by user action (browser stop button, closing tab, etc.)",
    );
  } else {
    // A network error, or some other problem.
    console.error(`Error: type: ${err.name}, message: ${err.message}`);
  }
}

Ein fetch mit Timeout oder explizitem Abbruch abbrechen

try {
  const controller = new AbortController();
  const timeoutSignal = AbortSignal.timeout(5000);
  const res = await fetch(url, {
    // This will abort the fetch when either signal is aborted
    signal: AbortSignal.any([controller.signal, timeoutSignal]),
  });
  const body = await res.json();
} catch (e) {
  if (e.name === "AbortError") {
    // Notify the user of abort.
  } else if (e.name === "TimeoutError") {
    // Notify the user of timeout
  } else {
    // A network error, or some other problem.
    console.log(`Type: ${e.name}, Message: ${e.message}`);
  }
}

Problematisches Muster: Listener wird nicht entfernt

const globalController = new AbortController();

async function doOperation(url, localSignal) {
  const signal = AbortSignal.any([globalController.signal, localSignal]);
  signal.addEventListener("abort", () => console.log(`Aborted: ${url}`), {
    once: true,
  });

  const response = await fetch(url, { signal });
  return response.text();
}

Listener im finally-Block entfernen

async function doOperation(url, localSignal) {
  const signal = AbortSignal.any([globalController.signal, localSignal]);
  const onAbort = () => console.log(`Aborted: ${url}`);
  signal.addEventListener("abort", onAbort, { once: true });

  try {
    const response = await fetch(url, { signal });
    return await response.text();
  } finally {
    signal.removeEventListener("abort", onAbort);
  }
}

Eine abbrechbare Promise-API implementieren

function myCoolPromiseAPI(/* …, */ { signal }) {
  return new Promise((resolve, reject) => {
    // If the signal is already aborted, immediately throw in order to reject the promise.
    signal.throwIfAborted();

    // Simulate the main operation completing after a delay.
    const timeoutId = setTimeout(() => {
      signal.removeEventListener("abort", onAbort);
      resolve("Operation completed");
    }, 1000);

    function onAbort() {
      // Stop the main operation and reject with the abort reason.
      clearTimeout(timeoutId);
      reject(signal.reason);
    }

    signal.addEventListener("abort", onAbort, { once: true });
  });
}

Verwendung der abbrechbaren API

const controller = new AbortController();
const signal = controller.signal;

startSpinner();

myCoolPromiseAPI({ /* …, */ signal })
  .then((result) => {})
  .catch((err) => {
    if (err.name === "AbortError") return;
    showUserErrorMessage();
  })
  .then(() => stopSpinner());

controller.abort();

// Wichtig · Fallstricke

Ein AbortSignal kann nur einmal verwendet werden. Nachdem es abgebrochen wurde, wird jeder fetch-Aufruf, der dasselbe Signal verwendet, sofort abgewiesen. Anders als bei AbortSignal.timeout() gibt es bei AbortSignal.any() keine Möglichkeit zu erkennen, ob der endgültige Abbruch durch einen Timeout verursacht wurde.