Skip to content

Instantly share code, notes, and snippets.

@AlCalzone
Last active February 7, 2023 10:38
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AlCalzone/d14b854b69ce5e8a03718336cc650a95 to your computer and use it in GitHub Desktop.
Save AlCalzone/d14b854b69ce5e8a03718336cc650a95 to your computer and use it in GitHub Desktop.
JavaScript: How to Async? (german)

How to? - Asynchroner Code

Kapitel 1: Synchroner Code

Ausgangslage: Synchroner Code. Einfach zu lesen, klarer Programmablauf:

const ergebnis1 = synchroneFunktion1();
const ergebnis2 = synchroneFunktion2();
tueWasMitErgebnis(ergebnis1, ergebnis2);

Problem 1: Node.js ist eventbasiert. Synchroner Code blockiert den Event-Loop, sprich anderer Code kann nicht parallel ablaufen.
Problem 2: Beide Funktionen laufen nacheinander ab, obwohl sie nicht voneinander abhängen.

Kapitel 2: Callbacks und die Callback-Hölle

Abhilfe schafft in Node.js die callback-Konvention:

callbackFunktion1((ergebnis1) => {
	callbackFunktion2((ergebnis2) => {
		tueWasMitErgebnis(ergebnis1, ergebnis2);
	});
});

Problem 1: gelöst.
Problem 2: Funktionen laufen immer noch nacheinander ab.
Problem 3: Code wird langsam unleserlich.


Um Problem 2 zu lösen, kann man die offenen Callbacks zählen, im Callback Zähler reduzieren, am besten noch den Call stack brechen mit setImmediate, etc...

function nachCallbacks() {
	tueWasMitErgebnis(ergebnis1, ergebnis2);
}
let ergebnis1, ergebnis2;
let offeneCallbacks = 0;
offeneCallbacks++;
callbackFunktion1((ergebnis) => {
	ergebnis1 = ergebnis;
	offeneCallbacks--;
	if (offeneCallbacks === 0) nachCallbacks();
});
callbackFunktion2((ergebnis) => {
	ergebnis2 = ergebnis;
	offeneCallbacks--;
	if (offeneCallbacks === 0) nachCallbacks();
});

Problem 1: gelöst.
Problem 2: gelöst.
Problem 3: Wir stecken richtig tief in der Callback-Hölle... 😨

Kapitel 3: Promises

Die Rettung Promises! Diese sind ein Versprechen auf einen Wert, der später kommt. Funktionen geben sofort einen Promise zurück, der später erfüllt wird. Ablaufsteuerung kann durch Verketten von Promises erfolgen, die dann nacheinander ausgeführt werden:

promiseFunktion1().then((ergebnis1) => {
	tueWasMitErgebnis1(ergebnis1);
});

Jede then-Funktion gibt wieder einen Promise zurück (explizit oder implizit), der mit der nächsten .then()-Funktion abgewartet werden kann.

promiseFunktion()
	.then((ergebnis1) => {
		const zwischenergebnis = tueWasMitErgebnis1(ergebnis1);
		return zwischenergebnis;
	})
	.then((zwischenergebnis) => {
		return zwischenergebnis + 1;
	})
	.then((zwischenergebnis2) => {
		// ...
	});

Fehler können über Promise rejections kommuniziert und mit .catch(error) behandelt werden:

promiseFunktion()
	.then((ergebnis1) => {
		// Fehler werfen:
		return Promise.reject(new Error("Warum?"));
	})
	.catch((fehler) => {
		// Fehler behandeln
	});

Achtung! Bitte nicht in Callback-Style verfallen. Also nicht so!

promiseFunktion().then((ergebnis1) => {
	return promiseFunktion2(ergebnis1).then((zwischenergebnis) => {
		return promiseFunktion3(zwischenergebnis).then((endergebnis) => {
			// BITTE NICHT!
			tueWasMitErgebnis(endergebnis);
		});
	});
});

Problem 1: gelöst.
Problem 2: wieder offen, wir können nur noch jeweils einen Wert weitergeben. Für mehrere Zwischenergebnisse müssen wir Objekte oder Arrays verwenden. Nicht schön...
Problem 3: besser, aber nicht gelöst.


Wenn die Ergebnisse aber nicht voneinander abhängig sind, können wir die Promises parallel ablaufen lassen:

const promise1 = promiseFunktion1();
const promise2 = promiseFunktion2();
// Beide Promises in ihre Ergebnis umwandeln:
Promise.all([promise1, promise2]).then(([ergebnis1, ergebnis2]) => {
	tueWasMitErgebnis(ergebnis1, ergebnis2);
});

Problem 1: gelöst
Problem 2: gelöst
Problem 3: noch besser, aber so richtig schön ist es immer noch nicht.

Kapitel 4: async/await to the rescue!

Die Lösung: async/await! Unter der Haube arbeiten immer noch Promises, aber wir werden von den ganzen Unschönheiten verschont.

async-Funktionen geben implizit einen Promise zurück, sehen aber aus wie eine synchrone Funktion:

async function asynchroneFunktion1() {
	return 1;
}

const promise1 = asynchroneFunktion1();
//    ^--- Der Rückgabewert ist ein Promise, nicht 1!

Fehler, die mit throw geworfen werden, werden ebenfalls implizit in einen rejected Promise umgewandelt. Dieser kann theoretisch wieder mit .catch() abgefangen werden:

async function asynchroneFunktion() {
  throw new Error("warum");
}

asynchroneFunktion().catch(e => /* Fehler behandeln */);

So haben wir aber noch gar nichts gewonnen, da immer noch die unschönen Promise-Ketten mit all ihren Problemen benötigt werden. Hierfür gibt es await - dieses kann aber nur innerhalb von async-Funktionen verwendet werden. Der Vorteil ist jetzt, dass der Code wieder aussieht als wäre er synchron:

async function main() {
	const ergebnis1 = await asynchroneFunktion1();
	const ergebnis2 = await asynchroneFunktion2();
	tueWasMitErgebnis(ergebnis1, ergebnis2);
}

await wandelt den zurückgegebenen Promise in den Wert um, den dieser (irgendwann später) enthält. Dazu wird die Funktion solange pausiert, bis der Wert bereit steht. Der Ablauf ist also exakt wie synchroner Code, nur dass parallel ablaufender Code nicht blockiert wird.

await kann für jede Funktion verwendet werden, die einen Promise zurückgibt. Die aufgerufene Funktion muss nicht zwangsläufig async sein. async ist nur für die Funktion nötig, in der das await-keyword verwendet werden soll.

Außerdem kann man wunderschön ein Array abarbeiten:

async function main() {
	const werte = [
		/* 1 Millionen Einträge */
	];
	let summe = 0;
	for (const wert of werte) {
		summe += await berechneWasMitWert(wert);
	}
	console.log(summe);
}

Fehlerbehandlung geht wie in synchronem Code (try/catch):

async function main() {
	try {
		await wirftVielleichtNenFehler();
	} catch (e) {
		// Fehler behandeln
	}
}

Ein Problem haben wir noch: Mehrere unabhängige async-Funktionen parallel ablaufen lassen. Da await "blockiert", wartet der Code, selbst wenn es unnötig ist. Abhilfe schafft wieder Promise.all(). Der folgende Code führt alle async-Funktionen parallel aus und speichert alle Ergebnisse in einem Array:

async function main() {
	// Ohne `await` werden Promises zurückgegeben und nicht gewartet!
	// Die Funktionen werden sozusagen gestartet.
	const promise1 = asynchroneFunktion1();
	const promise2 = asynchroneFunktion2();
	// ... ne for-Schleife würde es auch tun
	const promise10 = asynchroneFunktion10();

	// Am Ende kann auf alle Funktionen gewartet werden:
	const [ergebnis1, ergebnis2 /* ... */, , ergebnis10] = await Promise.all([
		promise1,
		promise2,
		// ...
		promise10,
	]);
}

Kapitel 5: Callback-APIs

Was, wenn wir mit callback-APIs oder timeouts arbeiten? Hier führt leider kein Weg daran vorbei, manuell einen Promise zu erstellen, und zwar so:

function tueEtwasMitCallbackAsync() {
	return new Promise((resolve, reject) => {
		// ... callback-API nutzen
	});
}

Die Funktion tueEtwasMitCallbackAsync kann dann mit await konsumiert werden.

resolve wird aufgerufen, wenn der Callback erfolgreich zurückgekommen ist, ob mit Wert oder ohne:

function tueEtwasMitCallbackAsync() {
	return new Promise((resolve) => {
		// ^ reject ist optional

		// ... callback-API nutzen
		// ein Wert kam zurück:
		resolve(wert);
		// oder: kein Wert kam zurück:
		resolve();
	});
}

reject ist optional und wird aufgerufen, wenn der Callback einen Fehler hatte:

function tueEtwasMitCallbackAsync() {
	return new Promise((resolve, reject) => {
		// ... callback-API nutzen
		reject(/* Fehler */);
	});
}

das könnte dann etwa so aussehen:

function tueEtwasMitCallbackAsync() {
	return new Promise((resolve, reject) => {
		callbackAPIAufrufen(1, 2, 3, (err, result) => {
			if (err) {
				reject(err);
			} else {
				resolve(result);
			}
		});
	});
}

Beim Implementieren darauf achten, dass nur der erste Aufruf von resolve oder reject etwas tut:

function funktion1() {
	return new Promise((resolve, reject) => {
		resolve(1);
		resolve();
		reject(new Error("nope"));
	});
}

async function main() {
	const result = await funktion1();
	//    ^-- result ist 1!
}

Kapitel 6: Ein paar Anti-Patterns

Es ist fast nie sinnvoll, Promise-Syntax mit await zu mischen. So ist nicht sofort klar, was in welcher Reihenfolge abläuft, daher für eine Konvention entscheiden! Im schlimmsten Fall vergisst man noch das await vor der Promise-Kette und wundert sich später über seltsamen Programmablauf.

// FALSCH:
const ergebnis = await methode1()
  .then(zwischenergebnis => zwischenergebnis + 1)
  .catch(e => /* Fehler behandeln */);

// RICHTIG:
try {
  const ergebnis = await methode1() + 1
} catch (e) {
  // Fehler behandlen
}

Versuchen, in den Ablauf innerhalb des Promise-Konstruktors einzugreifen (ist unnötig!)

// FALSCH
function funktion1() {
	return new Promise((resolve, reject) => {
		callbackFn1((err, result) => {
			if (err) {
				if (reject) reject(err);
				reject = null;
				resolve = null;
			} else {
				if (resolve) resolve(result);
				reject = null;
				resolve = null;
			}
		});
		callbackFn2((result) => {
			if (resolve) resolve(result);
			reject = null;
			resolve = null;
		});
	});
}

// RICHTIG -> der erste Aufruf von resolve oder reject gewinnt!
function funktion1() {
	return new Promise((resolve, reject) => {
		callbackFn1((err, result) => {
			if (err) {
				reject(err);
			} else {
				resolve(result);
			}
		});
		callbackFn2((result) => {
			resolve(result);
		});
	});
}
@Garfonso
Copy link

Top erklärt. 😃

vor await / async hab ich auch gerne sowas gemacht:

let promise = asynchroneFunktion1();

promise = promise.then(ergebnis => {
     return asynchroneFunktion2(ergebnis);
});

promise = promise.then(ergebnis2 => {
     return asynchroneFunktion3(ergebnis2);
});

usw...

promise = promise.then(endergebnis => {
    console.log("All done: " + endergebnis);
}, fehler => {
   console.error("Fehler: ", fehler);
});

das kam await schon recht nahe (ich mag diese .then() Verschachtelung ähnlich wenig wie callback Verschachtelung). Geht lustig nur schief, wenn man das promise = promise.then mal vergisst. flöt
Und ja, mehrere calls parallel mit callbacks sind die Hölle... 😃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment