Zum Inhalt springen
aviral gupta

// I6.3 · ca. 40 Min. · Aufbau

Anfragen mit AbortController abbrechen

Nach dieser Lektion können Sie eine Anfrage abbrechen, nur die neueste Suche gewinnen lassen und Ihren eigenen Promise-Funktionen ein Signal geben, dem sie gehorchen.

Lektion 3 von 5 in I6 Netzwerkanfragen und Speicher im Client

Danach können Sie

  • Ein fetch mit einem AbortController abbrechen und den AbortError, signal.aborted und signal.reason lesen
  • Ein Abbruchsignal mit einem Timeout über AbortSignal.any verbinden und eine ältere Suche abbrechen, wenn eine neue beginnt
  • Eine eigene Promise-Funktion mit einem abort-Listener abbrechbar machen, der mit signal.reason ablehnt
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus Lektion I1.5: ein in ein Promise verpacktes Ereignis. Was gibt das aus?

    function once(target, type) {
      return new Promise((resolve) => target.addEventListener(type, resolve, {once: true}));
    }
    const target = new EventTarget();
    const ping = once(target, "ping");
    target.dispatchEvent(new Event("ping"));
    target.dispatchEvent(new Event("ping"));
    console.log((await ping).type);
  2. Vorhersagen · Aufgabe 2 von 7

    Raten Sie, bevor Sie weiterlesen. api ist ein Testserver, dessen /slow nach 300 ms antwortet. Was gibt das aus?

    const controller = new AbortController();
    const request = fetch(api + "/slow", {signal: controller.signal});
    controller.abort();
    try {
      await request;
      console.log("loaded");
    } catch (error) {
      console.log(error.name, controller.signal.aborted);
    }
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie den Namen der fetch-Option, die die Anfrage mit dem Controller verbindet.

    ____: controller.signal
    : controller.signal
  4. Üben · Aufgabe 4 von 7

    abort() kann einen Grund annehmen. Was gibt das aus?

    const controller = new AbortController();
    console.log(controller.signal.aborted);
    controller.abort("user left the page");
    console.log(controller.signal.aborted, controller.signal.reason);
  5. Üben · Aufgabe 5 von 7

    Ein Signal für zwei Gründe zu stoppen. Was gibt das aus?

    const user = new AbortController();
    const signal = AbortSignal.any([user.signal, AbortSignal.timeout(1000)]);
    user.abort("cancelled");
    console.log(signal.aborted, signal.reason);
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. /search antwortet nach 50 ms. Zwei Suchen starten gleichzeitig. Was gibt das aus?

    let controller;
    async function search(term) {
      controller?.abort();
      controller = new AbortController();
      const response = await fetch(api + "/search?q=" + term, {signal: controller.signal});
      return response.json();
    }
    const results = await Promise.allSettled([search("e"), search("em")]);
    console.log(results.map((result) => result.status).join(" "));
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe: Schreiben Sie mit server.js aus dem durchgespielten Beispiel load(path, userSignal). Sie verbindet userSignal mit einem 100-ms-Timeout, holt api + path und gibt den Titel zurück, "cancelled" nach einem AbortError oder "too slow" nach einem TimeoutError. Probieren Sie /books/1, dann /slow mit einem Controller, den Sie nach 20 ms abbrechen, dann /slow ohne Abbruch.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Abbrechen, Timeout, die Neueste gewinnt und ein eigenes wait

server.js ist ein kleiner Testserver: /slow antwortet nach 300 ms und /search nach 50 ms. main.js bricht eine Anfrage ab, verbindet ein Abbruchsignal mit einem Timeout, führt drei Suchen aus, von denen nur die letzte fertig werden darf, und stoppt sein eigenes Promise-wait() mit einem Grund.

main.js

import {startServer} from "./server.js";

const server = await startServer();
const api = server.url;

// 1. Cancel one request.
const controller = new AbortController();
const request = fetch(api + "/slow", {signal: controller.signal});
setTimeout(() => controller.abort(), 20);
try {
  await request;
} catch (error) {
  console.log(error.name + ": " + error.message, controller.signal.aborted);
}

// 2. The user can cancel, and a timeout stops it anyway: whichever comes first.
const user = new AbortController();
const signal = AbortSignal.any([user.signal, AbortSignal.timeout(50)]);
try {
  await fetch(api + "/slow", {signal});
} catch (error) {
  console.log("any:", error.name);
}

// 3. Latest wins: a new search cancels the one before.
let current = null;
async function search(term) {
  current?.abort();
  current = new AbortController();
  const response = await fetch(api + "/search?q=" + term, {signal: current.signal});
  return response.json();
}
const results = await Promise.allSettled(["d", "du", "dun"].map(search));
for (const result of results) {
  console.log(result.status, result.status === "fulfilled" ? result.value : result.reason.name);
}

// 4. Your own promise-based function, cancellable.
function wait(ms, signal) {
  return new Promise((resolve, reject) => {
    signal.throwIfAborted(); // aborted already: reject at once
    const timer = setTimeout(() => resolve("done"), ms);
    signal.addEventListener("abort", () => {
      clearTimeout(timer);
      reject(signal.reason);
    }, {once: true});
  });
}
const stop = new AbortController();
setTimeout(() => stop.abort(new Error("Cancelled by the user")), 20);
try {
  await wait(1000, stop.signal);
} catch (error) {
  console.log("wait:", error.message);
}

await server.close();

server.js

// A small test server for this lesson, made with node:http. You do not need to change it.
import {createServer} from "node:http";

const TITLES = ["Dune", "Emma", "Kindred", "Beloved"];

export async function startServer() {
  const server = createServer((request, response) => {
    const url = new URL(request.url, "http://localhost");
    const json = (status, value) => {
      response.writeHead(status, {"Content-Type": "application/json"});
      response.end(JSON.stringify(value));
    };
    const later = (ms, value) => {
      const timer = setTimeout(() => json(200, value), ms);
      response.on("close", () => clearTimeout(timer)); // the client gave up
    };
    if (url.pathname === "/books/1") json(200, {id: 1, title: "Dune"});
    else if (url.pathname === "/slow") later(300, {id: 3, title: "Kindred"});
    else if (url.pathname === "/search") {
      // the titles that contain ?q=…, after 50 ms
      const term = (url.searchParams.get("q") ?? "").toLowerCase();
      later(50, TITLES.filter((title) => title.toLowerCase().includes(term)));
    } else json(404, {error: "Not found"});
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); // port 0: any free port
  return {
    url: `http://127.0.0.1:${server.address().port}`,
    close: () => new Promise((resolve) => server.close(resolve))
  };
}

Ausführen mit

node main.js

Ausgabe

AbortError: This operation was aborted true
any: TimeoutError
rejected AbortError
rejected AbortError
fulfilled [ 'Dune' ]
wait: Cancelled by the user
  • Die Nachricht „This operation was aborted“ stammt von Node.js; Browser formulieren sie anders, entscheiden Sie also nach error.name.
  • Das kombinierte Signal meldete TimeoutError, weil das Timeout vor jedem Abbruch kam.
  • wait() wurde mit dem Grund abgelehnt, der abort() übergeben wurde, einem eigenen Error, nicht einem AbortError.

Übungen

Übung 1 von 3

Eine Anfrage, die sich abbrechen lässt

loadBook(url, signal) soll url laden und das geparste JSON zurückgeben und stoppen, wenn signal abbricht: Sein Promise muss dann mit dem AbortError abgelehnt werden. Der Starter ignoriert das Signal, daher lädt eine abgebrochene Anfrage trotzdem. Übergeben Sie das Signal an fetch.

Diese Übung braucht Node.js auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.

Hinweise
  1. Hinweis 1

    Führen Sie die Tests aus: Das langsame Buch kommt nach abort() trotzdem an. Kennt fetch das Signal?

  2. Hinweis 2

    fetch nimmt das Signal in seinem Options-Objekt.

  3. Hinweis 3

    await fetch(url, {signal})

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

export async function loadBook(url, signal) {
  const response = await fetch(url, {signal});
  return response.json();
}
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

export async function loadBook(url, signal) {
  const response = await fetch(url);
  return response.json();
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {startServer} from './server.js';
import {loadBook} from './main.js';

const caught = (promise) => promise.then(() => null, (error) => error);

test('loadBook gives book 1', async () => {
  const server = await startServer();
  try {
    const book = await loadBook(server.url + '/books/1', new AbortController().signal);
    assert.deepEqual(book, {id: 1, title: 'Dune'}, `loadBook gave ${JSON.stringify(book)}`);
  } finally {
    await server.close();
  }
});

test('abort() makes loadBook reject with an AbortError', async () => {
  const server = await startServer();
  try {
    const controller = new AbortController();
    const loading = caught(loadBook(server.url + '/slow', controller.signal));
    setTimeout(() => controller.abort(), 20);
    const error = await loading;
    assert.equal(error?.name, 'AbortError', `loadBook should reject with an AbortError, but it gave ${error ? error.name : 'no error'}`);
  } finally {
    await server.close();
  }
});

server.js

// A small test server for this lesson, made with node:http. You do not need to change it.
import {createServer} from "node:http";

const TITLES = ["Dune", "Emma", "Kindred", "Beloved"];

export async function startServer() {
  const server = createServer((request, response) => {
    const url = new URL(request.url, "http://localhost");
    const json = (status, value) => {
      response.writeHead(status, {"Content-Type": "application/json"});
      response.end(JSON.stringify(value));
    };
    const later = (ms, value) => {
      const timer = setTimeout(() => json(200, value), ms);
      response.on("close", () => clearTimeout(timer)); // the client gave up
    };
    if (url.pathname === "/books/1") json(200, {id: 1, title: "Dune"});
    else if (url.pathname === "/slow") later(300, {id: 3, title: "Kindred"});
    else if (url.pathname === "/search") {
      // the titles that contain ?q=…, after 50 ms
      const term = (url.searchParams.get("q") ?? "").toLowerCase();
      later(50, TITLES.filter((title) => title.toLowerCase().includes(term)));
    } else json(404, {error: "Not found"});
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); // port 0: any free port
  return {
    url: `http://127.0.0.1:${server.address().port}`,
    close: () => new Promise((resolve) => server.close(resolve))
  };
}

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Übung 2 von 3

Die neueste Suche gewinnt

makeSearch(baseUrl) gibt eine Funktion search(term) zurück, die baseUrl + "/search?q=" + term lädt. Beginnt eine neue Suche, muss die vorige abgebrochen werden, sodass ihr Promise mit einem AbortError abgelehnt wird. Der Starter legt für jede Suche einen neuen Controller an, bricht den alten aber nie ab.

Diese Übung braucht Node.js auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.

Hinweise
  1. Hinweis 1

    Führen Sie die Tests aus: Die erste Suche wird trotzdem fertig. Wer könnte sie stoppen?

  2. Hinweis 2

    controller hält beim Start von search noch den Controller der vorigen Suche.

  3. Hinweis 3

    controller?.abort(); als erste Zeile von search

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

export function makeSearch(baseUrl) {
  let controller = null;
  return async function search(term) {
    controller?.abort();
    controller = new AbortController();
    const response = await fetch(baseUrl + "/search?q=" + term, {signal: controller.signal});
    return response.json();
  };
}
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

export function makeSearch(baseUrl) {
  let controller = null;
  return async function search(term) {
    controller = new AbortController();
    const response = await fetch(baseUrl + "/search?q=" + term, {signal: controller.signal});
    return response.json();
  };
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {startServer} from './server.js';
import {makeSearch} from './main.js';

const caught = (promise) => promise.then(() => null, (error) => error);

test('One search gives the matching titles', async () => {
  const server = await startServer();
  try {
    const search = makeSearch(server.url);
    const titles = await search('em');
    assert.deepEqual(titles, ['Emma'], `search('em') gave ${JSON.stringify(titles)}`);
  } finally {
    await server.close();
  }
});

test('A new search cancels the one before', async () => {
  const server = await startServer();
  try {
    const search = makeSearch(server.url);
    const first = caught(search('d'));
    const titles = await search('du');
    assert.deepEqual(titles, ['Dune'], `search('du') gave ${JSON.stringify(titles)}`);
    const error = await first;
    assert.equal(error?.name, 'AbortError', `the first search should reject with an AbortError, but it gave ${error ? error.name : 'no error'}`);
  } finally {
    await server.close();
  }
});

test('Of three quick searches only the last one finishes', async () => {
  const server = await startServer();
  try {
    const search = makeSearch(server.url);
    const results = await Promise.allSettled(['d', 'du', 'dun'].map(search));
    const states = results.map((result) => result.status);
    assert.deepEqual(states, ['rejected', 'rejected', 'fulfilled'], `the searches ended ${JSON.stringify(states)}`);
  } finally {
    await server.close();
  }
});

server.js

// A small test server for this lesson, made with node:http. You do not need to change it.
import {createServer} from "node:http";

const TITLES = ["Dune", "Emma", "Kindred", "Beloved"];

export async function startServer() {
  const server = createServer((request, response) => {
    const url = new URL(request.url, "http://localhost");
    const json = (status, value) => {
      response.writeHead(status, {"Content-Type": "application/json"});
      response.end(JSON.stringify(value));
    };
    const later = (ms, value) => {
      const timer = setTimeout(() => json(200, value), ms);
      response.on("close", () => clearTimeout(timer)); // the client gave up
    };
    if (url.pathname === "/books/1") json(200, {id: 1, title: "Dune"});
    else if (url.pathname === "/slow") later(300, {id: 3, title: "Kindred"});
    else if (url.pathname === "/search") {
      // the titles that contain ?q=…, after 50 ms
      const term = (url.searchParams.get("q") ?? "").toLowerCase();
      later(50, TITLES.filter((title) => title.toLowerCase().includes(term)));
    } else json(404, {error: "Not found"});
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); // port 0: any free port
  return {
    url: `http://127.0.0.1:${server.address().port}`,
    close: () => new Promise((resolve) => server.close(resolve))
  };
}

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Übung 3 von 3

Ein wait, das sich abbrechen lässt

wait(ms, signal) wird nach ms Millisekunden mit "done" erfüllt. Machen Sie es abbrechbar: Bricht signal ab, löschen Sie den Timer und lehnen mit signal.reason ab. Wurde das Signal schon vor dem Aufruf von wait abgebrochen, lehnen Sie sofort mit seinem Grund ab. Der Starter ignoriert das Signal.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Führen Sie die Tests aus: wait wird nach abort() trotzdem erfüllt. Wer sagt dem Promise etwas vom Abbruch?

  2. Hinweis 2

    signal.addEventListener("abort", () => { … }, {once: true}) im Executor, der auch reject braucht.

  3. Hinweis 3

    Im Listener: clearTimeout(timer); reject(signal.reason);. Vor dem Timer: if (signal.aborted) { reject(signal.reason); return; }

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

export function wait(ms, signal) {
  return new Promise((resolve, reject) => {
    if (signal.aborted) {
      reject(signal.reason);
      return;
    }
    const timer = setTimeout(() => resolve("done"), ms);
    signal.addEventListener("abort", () => {
      clearTimeout(timer);
      reject(signal.reason);
    }, {once: true});
  });
}
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

export function wait(ms, signal) {
  return new Promise((resolve) => {
    setTimeout(() => resolve("done"), ms);
  });
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {wait} from './main.js';

test('wait resolves with done when nobody aborts', async () => {
  const value = await wait(10, new AbortController().signal);
  assert.equal(value, 'done', `wait gave ${value}`);
});

test('abort() makes wait reject with an AbortError', async () => {
  const controller = new AbortController();
  setTimeout(() => controller.abort(), 10);
  await assert.rejects(wait(200, controller.signal), {name: 'AbortError'}, 'wait should reject with an AbortError after abort()');
});

test('An aborted signal makes wait reject with its reason', async () => {
  const controller = new AbortController();
  controller.abort(new Error('too late'));
  await assert.rejects(wait(200, controller.signal), {message: 'too late'}, 'wait should reject with the reason of a signal that is already aborted');
});

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Häufige Fehler

Einen abgebrochenen Controller wiederverwenden

import {startServer} from "./server.js";

const server = await startServer();
const controller = new AbortController();
try {
  const first = fetch(server.url + "/slow", {signal: controller.signal});
  controller.abort(); // the user cancels the first request
  await first.catch((error) => console.log("first:", error.name));
  const response = await fetch(server.url + "/books/1", {signal: controller.signal});
  console.log((await response.json()).title);
} finally {
  await server.close();
}

Was Node.js ausgibt

DOMException [AbortError]: This operation was aborted

Warum, und die Lösung

Ein Signal wird einmal verwendet: Nach abort() bleibt es abgebrochen, und jedes fetch, das es bekommt, wird sofort abgelehnt. Legen Sie für jede Anfrage einen neuen AbortController an: const next = new AbortController(); fetch(url, {signal: next.signal}).

abort() auf dem Signal aufrufen

const controller = new AbortController();
controller.signal.addEventListener("abort", () => console.log("aborted"));
controller.signal.abort();

Was Node.js ausgibt

TypeError: controller.signal.abort is not a function

Warum, und die Lösung

Das Signal hört nur zu; der Controller entscheidet. Rufen Sie controller.abort() auf. Geben Sie nur das Signal an den Code weiter, der die Arbeit erledigt, und behalten Sie den Controller dort, wo die Entscheidung zum Abbrechen fällt.

AbortSignal.any ohne Array

const user = new AbortController();
const signal = AbortSignal.any(user.signal, AbortSignal.timeout(5000));
console.log(signal.aborted);

Was Node.js ausgibt

TypeError: signals cannot be converted to sequence.

Warum, und die Lösung

AbortSignal.any nimmt ein Argument: ein Array (oder ein anderes Iterable) von Signalen. Setzen Sie die Signale in eckige Klammern: AbortSignal.any([user.signal, AbortSignal.timeout(5000)]). Diese Meldung stammt von Node.js; ein Browser formuliert sie anders.

JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Ein Controller und sein Signal

const controller = new AbortController() gibt Ihnen zwei Teile: controller.signal, das Sie der Arbeit übergeben, wie in fetch(url, {signal: controller.signal}), und controller.abort(), das Sie aufrufen, um sie zu stoppen. fetch wird dann mit signal.reason abgelehnt, einer DOMException namens AbortError, außer Sie übergeben einen eigenen Grund, wie in abort(new Error("Page closed")). signal.aborted wird true und bleibt true: Ein Signal wird einmal verwendet. Ein späteres fetch mit demselben Signal wird sofort abgelehnt, legen Sie also für jede Anfrage einen neuen Controller an.

Mehrere Gründe zu stoppen

AbortSignal.any([user.signal, AbortSignal.timeout(5000)]) erzeugt ein Signal, das abbricht, sobald irgendein Signal im Array abbricht, mit dem Grund dieses Signals. Ein fetch stoppt also entweder, wenn die Nutzerin oder der Nutzer auf Abbrechen klickt (AbortError), oder wenn die Zeit um ist (TimeoutError). Ein Suchfeld braucht „die Neueste gewinnt“: Brechen Sie den Controller der vorigen Suche ab, bevor eine neue beginnt. Sonst kann eine alte Antwort, die spät eintrifft, die Ergebnisse der neueren Suche überschreiben.

Auch Ihre eigenen Funktionen können zuhören

Ein Signal ist ein EventTarget. signal.addEventListener("abort", listener, {once: true}) führt den Listener aus, wenn abort() aufgerufen wird. Um eine Promise-Funktion abbrechbar zu machen, wie wait(ms) aus Lektion I1.5, nehmen Sie ein Signal und räumen im Listener auf (clearTimeout) und rufen reject(signal.reason) auf. Prüfen Sie auch das Signal vorab: Wurde es vor Ihrem Start abgebrochen, ist das Ereignis schon gefeuert und feuert nicht noch einmal. signal.throwIfAborted() wirft in diesem Fall den Grund.

Quellen

Zuletzt geprüft am 5. Oktober 2026