Zum Inhalt springen
aviral gupta

// I1.2 · ca. 32 Min. · Aufbau

Von Callbacks zu Promises

Nach dieser Lektion machen Sie aus verschachtelten Callbacks eine flache Promise-Kette, die auf jeden Schritt wartet, alle Fehler in einem catch() behandelt und in finally() aufräumt.

Lektion 2 von 6 in I1 Asynchrones JavaScript

Danach können Sie

  • Die Zustände eines Promise benennen und sagen, wann seine then()-Handler laufen
  • then()-Aufrufe verketten, die Werte und Promises zurückgeben, sodass jeder Schritt auf den vorigen wartet
  • Jeden Fehler einmal mit catch() behandeln, danach weitermachen und mit finally() aufräumen
  1. Aufwärmen · Aufgabe 1 von 7

    Zum Aufwärmen aus Lektion I1.1: ein Callback für setTimeout. Was gibt dieser Code aus?

    console.log("order");
    setTimeout(() => console.log("ready"), 0);
    console.log("wait");
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen: Was gibt dieser Code aus?

    const first = Promise.resolve(2);
    const second = first.then((n) => n * 10);
    console.log(first === second);
    second.then((n) => console.log(n));
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie die Methode, die eine Ablehnung an beliebiger früherer Stelle der Kette behandelt, sodass „failed: timeout“ ausgegeben wird.

    Promise.reject(new Error("timeout"))
      .then((data) => console.log("data:", data))
      .____((error) => console.log("failed:", error.message));
    .((error) => console.log("failed:", error.message));
  4. Üben · Aufgabe 4 von 7

    Schritt 2 wirft einen Fehler. Bringen Sie die Zeilen, die dieses Programm ausgibt, in die Reihenfolge, in der sie erscheinen.

    Promise.resolve()
      .then(() => console.log("step 1"))
      .then(() => {
        throw new Error("step 2 broke");
      })
      .then(() => console.log("step 3"))
      .catch((error) => console.log("caught:", error.message))
      .then(() => console.log("after the catch"))
      .finally(() => console.log("cleanup"));
    1. 1.step 1
    2. 2.cleanup
    3. 3.after the catch
    4. 4.caught: step 2 broke
  5. Üben · Aufgabe 5 von 7

    Der erste Callback gibt ein Promise zurück. Was erhält der zweite Callback?

    const later = (value) => new Promise((resolve) => setTimeout(() => resolve(value), 10));
    
    Promise.resolve("id-7")
      .then((id) => later(`user for ${id}`))
      .then((result) => console.log(typeof result, result));
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. save() braucht 10 ms. Was gibt dieser Code aus?

    const save = (x) => new Promise((resolve) => setTimeout(() => resolve(`saved ${x}`), 10));
    
    Promise.resolve("draft")
      .then((doc) => {
        save(doc);
      })
      .then((result) => console.log("result:", result));
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe. Schreiben Sie drei verschachtelte setTimeout-Callbacks, die im Abstand von 10 ms wash, dry und fold ausgeben. Schreiben Sie sie dann als flache Kette um: eine Hilfsfunktion after(ms, label), die nach ms Millisekunden ein Promise von label liefert, then()-Schritte, die jeweils ausgeben und das nächste after() zurückgeben, ein catch() am Ende und ein finally(), das laundry done ausgibt. Führen Sie beide Versionen aus.

    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

Ein Benutzer und seine Bestellungen als Kette

findUser und findOrders geben jeweils ein Promise zurück, das etwas später abgeschlossen wird. Das Programm zeigt, dass der Aufruf sofort ein Promise zurückgibt, und verkettet dann die Schritte: Jedes then() erhält den Wert des vorigen Schritts, das erste gibt das nächste Promise zurück, damit die Kette darauf wartet, und catch() und finally() schließen die Kette ab. Die zweite Hälfte fragt nach Benutzer 2, den es nicht gibt, also überspringt die Ablehnung ein then() und erreicht das letzte catch().

main.js

// Two steps that finish later. Each returns a promise (lesson I1.5 shows how to make one).
function findUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => (id === 1 ? resolve({id, name: "Ada"}) : reject(new Error(`no user ${id}`))), 20);
  });
}
function findOrders(user) {
  return new Promise((resolve) => setTimeout(() => resolve([`${user.name}-A`, `${user.name}-B`]), 10));
}

const pending = findUser(1);
console.log("right after the call:", pending instanceof Promise);

pending
  .then((user) => {
    console.log("user:", user.name);
    return findOrders(user); // return the next promise, so the chain waits for it
  })
  .then((orders) => {
    console.log("orders:", orders);
    return orders.length;
  })
  .then((count) => console.log("count:", count))
  .catch((error) => console.log("failed:", error.message))
  .finally(() => console.log("done with user 1"))
  .then(() => findUser(2))
  .then((user) => console.log("never printed:", user.name))
  .catch((error) => console.log("failed:", error.message));

Ausführen mit

node main.js

Ausgabe

right after the call: true
user: Ada
orders: [ 'Ada-A', 'Ada-B' ]
count: 2
done with user 1
failed: no user 2
  • Die erste Zeile erscheint vor jedem Schritt: Ein Promise wird sofort zurückgegeben, noch pending.
  • Die Bestellungen kommen nur an, weil der erste Callback findOrders(user) zurückgegeben hat.
  • finally() hat nach dem erfolgreichen Teil ausgegeben und die Kette weitergereicht.
  • „never printed“ wird übersprungen: Die Ablehnung von findUser(2) springt zum nächsten catch().
Ändern und ausführen

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.

Übungen

Übung 1 von 2

Den nächsten Schritt zurückgeben

orderTotal(fetchPrice, fetchShipping) soll ein Promise von Preis plus Versand zurückgeben. fetchPrice() gibt ein Promise des Preises zurück; fetchShipping(price) ein Promise der Versandkosten für diesen Preis. Der Starter ruft beide auf, aber sein Promise wird mit undefined erfüllt. Sorgen Sie dafür, dass die Kette auf den Versand wartet und die Summe zurückgibt. Eine Ablehnung von fetchPrice() muss orderTotal weiterhin ablehnen.

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: Die Summe ist undefined. Was gibt der äußere Callback zurück?

  2. Hinweis 2

    Der Callback hat geschweifte Klammern, also gibt er nur zurück, was eine return-Anweisung ihm gibt.

  3. Hinweis 3

    Schreiben Sie return vor fetchShipping(price).then(…), damit das äußere then() auf dieses Promise wartet.

Eine Lösung zeigen

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

// Resolve to price + shipping.
export function orderTotal(fetchPrice, fetchShipping) {
  return fetchPrice().then((price) => {
    return fetchShipping(price).then((shipping) => price + shipping);
  });
}
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

// Resolve to price + shipping.
export function orderTotal(fetchPrice, fetchShipping) {
  return fetchPrice().then((price) => {
    fetchShipping(price).then((shipping) => price + shipping);
  });
}

main.test.js

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

const later = (value) => new Promise((resolve) => setTimeout(() => resolve(value), 5));
const shipping = (price) => later(price >= 50 ? 0 : 4.5);

test('A price of 20 with 4.50 shipping gives 24.5', async () => {
  const got = await orderTotal(() => later(20), shipping);
  assert.equal(got, 24.5, `orderTotal gave ${got}, expected 24.5`);
});

test('A price of 60 ships free, so the total is 60', async () => {
  const got = await orderTotal(() => later(60), shipping);
  assert.equal(got, 60, `orderTotal gave ${got}, expected 60`);
});

test('A rejected price rejects the total with the same error', async () => {
  const failing = () => Promise.reject(new Error('price service down'));
  await assert.rejects(orderTotal(failing, shipping), {message: 'price service down'}, 'orderTotal should reject with the price error');
});

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 2

Ein catch und ein finally

loadName(fetchUser, log) soll ein Promise des Benutzernamens in Großbuchstaben zurückgeben, oder GUEST, wenn fetchUser() abgelehnt wird oder der Benutzer keinen Namen hat. In jedem Fall muss es log("loaded") genau einmal aufrufen. Der Starter übergibt einen Fehler-Handler als zweites Argument von then(), der keine Fehler aus dem ersten sieht, und er loggt nur bei Erfolg. Nutzen Sie catch() und finally().

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: Der Benutzer ohne Namen scheitert mit einem TypeError. Welcher Handler sollte ihn sehen?

  2. Hinweis 2

    then(onOk, onFail): onFail behandelt nur das Promise, auf dem then() aufgerufen wurde, nicht Fehler, die in onOk geworfen werden.

  3. Hinweis 3

    Hängen Sie .catch(() => "GUEST") hinter das then() und setzen Sie log("loaded") in .finally(…), das auf beiden Wegen läuft.

Eine Lösung zeigen

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

// Resolve to the name in capitals, or GUEST; call log("loaded") once either way.
export function loadName(fetchUser, log) {
  return fetchUser()
    .then((user) => user.name.toUpperCase())
    .catch(() => "GUEST")
    .finally(() => log("loaded"));
}
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

// Resolve to the name in capitals, or GUEST; call log("loaded") once either way.
export function loadName(fetchUser, log) {
  return fetchUser()
    .then((user) => user.name.toUpperCase(), () => "GUEST")
    .then((name) => {
      log("loaded");
      return name;
    });
}

main.test.js

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

const run = async (fetchUser) => {
  const lines = [];
  const name = await loadName(fetchUser, (line) => lines.push(line));
  return {name, lines};
};

test('A user named ada gives ADA and logs loaded once', async () => {
  const {name, lines} = await run(() => Promise.resolve({name: 'ada'}));
  assert.equal(name, 'ADA', `the name was ${name}, expected ADA`);
  assert.deepEqual(lines, ['loaded'], `log got ${JSON.stringify(lines)}, expected ["loaded"]`);
});

test('A rejected fetchUser gives GUEST and logs loaded once', async () => {
  const {name, lines} = await run(() => Promise.reject(new Error('offline')));
  assert.equal(name, 'GUEST', `the name was ${name}, expected GUEST`);
  assert.deepEqual(lines, ['loaded'], `log got ${JSON.stringify(lines)}, expected ["loaded"]`);
});

test('A user without a name gives GUEST and logs loaded once', async () => {
  const {name, lines} = await run(() => Promise.resolve({}));
  assert.equal(name, 'GUEST', `the name was ${name}, expected GUEST`);
  assert.deepEqual(lines, ['loaded'], `log got ${JSON.stringify(lines)}, expected ["loaded"]`);
});

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

Das Promise so verwenden, als wäre es der Wert

const findUser = (id) => Promise.resolve({id, name: "Ada"});

const user = findUser(1);
console.log(user.name.toUpperCase());

Was Node.js ausgibt

TypeError: Cannot read properties of undefined (reading 'toUpperCase')

Warum, und die Lösung

findUser gibt ein Promise zurück, nicht den Benutzer. Ein Promise hat keine Eigenschaft name, also ist user.name undefined. Den Wert gibt es erst in einem then()-Callback (oder nach await, Lektion I1.3): findUser(1).then((user) => console.log(user.name.toUpperCase())).

Ein then()-Callback, der das return vergisst

Promise.resolve([3, 4])
  .then((items) => {
    items.map((n) => n * 2);
  })
  .then((doubled) => console.log(doubled.length));

Was Node.js ausgibt

TypeError: Cannot read properties of undefined (reading 'length')

Warum, und die Lösung

Ein Callback mit geschweiften Klammern gibt undefined zurück, solange er nicht return sagt, also erhält der nächste Schritt undefined. Nichts fängt den TypeError ab, also stoppt der Lauf damit. Schreiben Sie return items.map((n) => n * 2); oder lassen Sie die Klammern weg. Dieselbe Regel lässt eine Kette warten: Geben Sie das nächste Promise zurück.

Eine Kette ohne catch()

const load = () => Promise.reject(new Error("offline"));

load().then((data) => console.log("data:", data));
console.log("started");

Was Node.js ausgibt

Error: offline

Warum, und die Lösung

Ohne catch() kann eine Ablehnung nirgendwohin. Node.js meldet eine unbehandelte Ablehnung und stoppt mit Exit-Code 1, nachdem „started“ ausgegeben wurde; ein Browser schreibt sie in die Konsole. Beenden Sie jede Kette mit catch() oder geben Sie die Kette an einen Aufrufer zurück, der sie behandelt: load().then(…).catch((error) => console.log("failed:", error.message)).

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

Callbacks verschachteln sich

Ein Callback ist eine Funktion, die Sie einer anderen Funktion übergeben und die diese später aufruft: setTimeout(done, 100) ruft done nach etwa 100 ms auf. Braucht Schritt zwei das Ergebnis von Schritt eins, steht sein Aufruf im Callback von Schritt eins, Schritt drei wiederum darin, und der Code wandert nach rechts. MDN nennt das „callback hell“ oder „pyramid of doom“ und nennt auch den zweiten Preis: Fehler müssen Sie oft auf jeder Ebene der Pyramide behandeln statt nur einmal.

Ein Promise ist ein Ergebnis, das später kommt

Ein Promise ist ein Objekt für das Ergebnis einer Arbeit, die noch läuft. Es beginnt als pending und wird genau einmal abgeschlossen: fulfilled mit einem Wert oder rejected mit einem Grund, meist einem Error. Danach ändert es sich nie mehr. Handler hängen Sie mit then(onFulfilled, onRejected) an. Wie Lektion I1.1 gezeigt hat, laufen sie nie synchron, auch nicht bei einem Promise, das schon abgeschlossen ist, und mehrere Handler an einem Promise laufen in der Reihenfolge, in der Sie sie angehängt haben.

Ketten: return, ein catch, finally

then() gibt sofort ein neues Promise zurück. Es wird mit dem erfüllt, was Ihr Callback zurückgibt; gibt der Callback ein Promise zurück, wartet die Kette darauf, und das nächste then() erhält dessen Wert. Geben Sie also immer den nächsten Schritt zurück: Ein Callback mit geschweiften Klammern und ohne return reicht undefined weiter, und sein Promise ist „floating“. Ein throw oder eine Ablehnung überspringt die folgenden then()-Callbacks und landet im nächsten catch(). Nach einem catch() kann die Kette weitergehen. finally() läuft in beiden Fällen, erhält kein Argument und reicht das Ergebnis durch.

Quellen

Zuletzt geprüft am 5. Oktober 2026