Abschnitt 4 · Lektion 3
Exit-Codes und stderr
Dein Programm wird selten von einem Menschen gestartet. Meistens ruft es ein anderes Programm auf: ein Skript, ein Build, ein Cronjob, eine Pipeline. Und die lesen nicht, was du schreibst. Sie schauen auf eine Zahl.
Das ist eine andere Denkweise als im Browser, und sie fehlt fast allen, die von dort kommen. Dort ist die Ausgabe für Augen. Hier ist sie für ein Programm.
Die Zahl am Ende
Jeder Prozess hinterlässt beim Beenden einen Exit-Code, eine Zahl zwischen 0 und 255.
0 heißt: hat geklappt. Jede andere Zahl heißt: hat nicht geklappt. Mehr steckt in der Verabredung nicht drin. Welche Zahl du für welchen Fehler nimmst, ist deine Sache, und die meisten Werkzeuge kommen mit 1 aus.
Das ist der Grund, warum befehl-a && befehl-b den zweiten Befehl nur ausführt, wenn der erste eine
0 hinterlassen hat. Und der Grund, warum deine CI-Pipeline rot wird: Irgendein Schritt hat etwas
anderes als 0 zurückgegeben.
Kommt dein Skript ohne Absturz bis zur letzten Zeile, ist der Code von selbst 0. Du musst dafür nichts tun. Interessant wird es erst, wenn du Misserfolg melden willst.
Zwei Wege, Misserfolg zu melden
Der erste ist process.exit(1). Der beendet den Prozess sofort.
Und zwar wirklich sofort, mitten in allem, was gerade läuft.
console.log("Start");
setTimeout(() => {
console.log("Aufraeumen erledigt");
}, 0);
process.exit(1); Das setTimeout war angemeldet, es kommt nie dazu. Die Zeile „Aufraeumen erledigt” wird nie
geschrieben. Bei einem Aufräumschritt ist das ärgerlich, bei einer laufenden Schreiboperation ist es
richtig schlecht: Die Datei bleibt halb geschrieben liegen.
Der zweite Weg ist process.exitCode = 1.
console.log("Start");
setTimeout(() => {
console.log("Aufraeumen erledigt");
}, 0);
process.exitCode = 1; Derselbe Code, ein Wort anders. Hier wird die Zahl nur hinterlegt. Node arbeitet ab, was noch offen ist, und benutzt sie dann beim regulären Ende. Der Timeout kommt dran, die Ausgabe ist vollständig, und der Exit-Code ist trotzdem 1.
Nimm im Zweifel process.exitCode. Es tut dasselbe, ohne dir etwas abzuschneiden.
process.exit() ist dann richtig, wenn du wirklich sofort weg willst: bei einem Fehler ganz am
Anfang, wenn ein Pflichtargument fehlt und noch gar nichts läuft. Genau so hast du es in der
Challenge zu Lektion 4.1 benutzt, und dort war es die passende Wahl.
Zwei getrennte Ausgabekanäle
Jetzt der zweite Teil, und er hängt mit dem ersten enger zusammen, als es aussieht.
Ein Prozess hat nicht einen Ausgabekanal, sondern zwei. console.log schreibt auf stdout, die
Standardausgabe. console.error schreibt auf stderr, den Fehlerkanal.
Auf dem Bildschirm landen beide untereinander, deshalb sieht man den Unterschied dort nicht.
console.log("Ergebnis: 42 Zeilen");
console.error("Hinweis: 2 Dateien uebersprungen");
console.log("Fertig."); Drei Zeilen, aber zwei Kanäle: „Ergebnis: 42 Zeilen” und „Fertig.” gehen auf stdout, der Hinweis dazwischen auf stderr. Du siehst das hier selbst: Das Terminal hält die beiden auseinander. Oben steht, was auf stdout ging, darunter in Rot der Fehlerkanal. Beschriftet ist keiner von beiden, die Farbe ist die ganze Auskunft, und genauso hält es eine Shell. Ein anderes Programm bekommt sie ebenso getrennt zu fassen.
Warum das mehr ist als Kosmetik, zeigt eine Umleitung:
node bericht.mjs > ergebnis.txt
In der Datei landen nur die beiden stdout-Zeilen. Der Hinweis bleibt auf dem Bildschirm stehen, wo du ihn siehst. Das ist kein Zufall, sondern der ganze Zweck der Trennung.
Und damit lässt sich etwas bauen, das nach mehr aussieht, als es ist:
// So sieht ein Werkzeug aus, das seine Trennung ernst nimmt. Gestartet
// wird es mit: node bericht.mjs > bericht.html 2> lauf.log
const dateien = [
{ name: "januar.csv", saetze: 1204, fehler: 0 },
{ name: "februar.csv", saetze: 987, fehler: 3 },
{ name: "maerz.csv", saetze: 1130, fehler: 0 },
];
// Der Ablauf. Gehoert dem Menschen davor, also stderr.
dateien.forEach((d, i) => {
console.error(`Lese ${d.name} (${i + 1} von ${dateien.length})`);
});
// Das Ergebnis. Gehoert in die Datei, also stdout.
console.log("<style>table{border-collapse:collapse}td,th{border:1px solid #999;padding:4px 10px}</style>");
console.log("<h1>Auswertung Q1</h1>");
console.log("<table>");
console.log("<tr><th>Datei</th><th>Saetze</th><th>Fehler</th></tr>");
for (const d of dateien) {
console.log(`<tr><td>${d.name}</td><td>${d.saetze}</td><td>${d.fehler}</td></tr>`);
}
console.log("</table>");
console.error("Fertig, 3 Dateien."); Das Programm schreibt seinen Fortschritt auf stderr und den fertigen Bericht auf stdout, sonst nichts. Im Terminal stehen beide getrennt: oben das Markup, darunter in Rot die vier Zeilen über den Ablauf. Der Reiter „Browser” bleibt dabei leer, und das ist richtig so: Hier läuft kein Server, es gibt also nichts, was ein Browser laden könnte. Was aus dem Markup wird, siehst du gleich selbst, indem du es in eine Datei schreibst.
Das ist keine Spielerei der Lernumgebung, sondern genau das, was > bericht.html auf deinem eigenen
Rechner täte. Und hier kannst du das wirklich tun. Starte die Sandbox, hol dieses Beispiel nach vorn
und tipp im Terminal den Aufruf, der auch im Kommentar über dem Code steht:
node bericht.mjs > bericht.html 2> lauf.log
Der Befehl schweigt, beide Kanäle sind ja umgeleitet. Danach zeigt ls drei Dateien statt einer,
cat lauf.log gibt dir die vier Ablaufzeilen und head -2 bericht.html den Anfang des Berichts. Was
wo gelandet ist, hast du damit nicht gelesen, sondern nachgesehen.
Dasselbe gilt für eine Kette: node erzeuge.mjs | node verarbeite.mjs reicht nur stdout weiter.
Alles, was du auf stderr schreibst, stört die Kette nicht.
Die Regel
Auf stdout gehört das Ergebnis. Auf stderr gehört alles über den Ablauf.
Ergebnis heißt: das, wofür dein Programm aufgerufen wurde. Die umgerechnete Zahl, die erzeugte Liste, das JSON.
Ablauf heißt: Fortschrittsmeldungen, Warnungen, Fehler, „lese Datei 3 von 40”. Nützlich für den Menschen davor, aber nichts, was jemand in eine Datei schreiben will.
Der Test dafür ist einfach: Stell dir vor, jemand leitet deine Ausgabe in eine Datei um. Alles, was dann drinstehen soll, gehört auf stdout. Der Rest nicht.
Ein Fehler dabei fällt lange nicht auf, weil auf dem Bildschirm ja alles da ist. Er fällt genau dann auf, wenn jemand dein Werkzeug zum ersten Mal in ein Skript einbaut, und dann steht mitten in der erzeugten Datei „Verarbeite Datei 3 von 40”.
Zum Mitnehmen
Ergebnisse auf stdout, alles über den Ablauf auf stderr. Und 0 heißt erfolgreich, jede andere Zahl heißt es nicht.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.