Dateien
Kurz gesagt: Eine Datei behält die Daten auf der Festplatte, auch nachdem das Programm endet; in Node.js liest und schreibt das Modul node:fs Dateien, und Streams lesen eine große Datei Zeile für Zeile.
Problem
Das Programm vergisst
Du schreibst ein Programm für deine Ausgaben. Du gibst sie abends ein und schließt es, und am nächsten Tag weiß es nichts mehr von gestern. Hier ist das kürzeste Beispiel:
let visits = 0;visits++;console.log(`Gestartet: ${visits} Mal.`);Gestartet: 1 Mal.Egal wie oft du es startest, es sagt „1 Mal“. Variablen leben im Speicher, solange das Programm läuft. Wenn es endet, wird der Speicher freigegeben, und alles verschwindet.
Damit es sich erinnert, muss das Programm die Daten dort ablegen, wo sie auch nach seinem Ende bleiben: in einer Datei auf der Festplatte.
Die Idee
Eine Datei mit node:fs
Hier ist ein ganzes Programm, das eine Notiz in eine Datei schreibt und sie wieder liest:
import { readFileSync, writeFileSync } from "node:fs";
try { writeFileSync("notes.txt", "Milch kaufen\nStromrechnung bis zum 15. bezahlen\n"); const text = readFileSync("notes.txt", "utf8"); process.stdout.write(text);} catch (e) { console.log(`Problem mit der Datei: ${e.message}`);}Milch kaufenStromrechnung bis zum 15. bezahlenimport { readFileSync, writeFileSync } from "node:fs";nimmt zwei Funktionen aus dem Modulnode:fs(file system, „das Dateisystem“). Es kommt mit Node, und du installierst es nicht.writeFileSync(name, text)schreibt den Text und legt die Datei an, wenn es sie nicht gibt."notes.txt"ist eine Datei mit diesem Namen in dem Ordner, aus dem das Programm gestartet wurde.readFileSync(name, "utf8")liest die ganze Datei als Text."utf8"ist die Kodierung: wie Bytes zu Buchstaben werden.- „Sync“ heißt, dass das Programm wartet, bis die Datei geschrieben oder gelesen ist, und erst dann weitermacht.
- Wenn etwas schiefgeht (die Festplatte ist voll, die Datei ist gesperrt oder nicht da), wirft die Funktion einen Fehler. JavaScript zwingt dich nicht, ihn zu fangen, aber ohne
catchhält das Programm an. process.stdout.write(text)gibt den Text so aus, wie er ist.console.logwürde am Ende noch einen Zeilenumbruch anhängen.
Starte das Programm und öffne dann den Ordner: notes.txt ist da, und du kannst sie mit jedem Texteditor öffnen. Der nächste Start findet sie.
In jedem Beispiel ab hier stehen die imports oben: Aus dem Modul nimmst du nur die Funktionen, die du verwendest.
Die Idee
Zeilen anhängen und lesen
Ausgaben kommen eine nach der anderen dazu. appendFileSync hängt am Ende an, statt die Datei neu zu schreiben. split("\n") teilt den gelesenen Text in Zeilen:
import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
try { writeFileSync("expenses.txt", "Mittagessen;12.50\n"); appendFileSync("expenses.txt", "Taxi;8.20\n"); appendFileSync("expenses.txt", "Buch;18.90\n"); const lines = readFileSync("expenses.txt", "utf8").trimEnd().split("\n"); let total = 0; for (const line of lines) { const parts = line.split(";"); total += Number(parts[1]); console.log(`${parts[0]}: ${parts[1]} €`); } console.log(`${lines.length} Ausgaben, gesamt ${total.toFixed(2)} €`);} catch (e) { console.log(`Problem mit der Datei: ${e.message}`);}Mittagessen: 12.50 €Taxi: 8.20 €Buch: 18.90 €3 Ausgaben, gesamt 39.60 €lines ist ein Array von Strings, einer für jede Zeile. trimEnd() vor split entfernt das letzte Zeichen für den Zeilenumbruch. Ohne es hätte das Array am Ende noch ein Element: einen leeren String "".
Genauer, für Neugierige
appendFileSynclegt die Datei an, wenn es sie nicht gibt, deshalb gibt das erste Anhängen keinen Fehler.- Ohne
"utf8"gibtreadFileSynceinenBufferzurück: die Bytes der Datei selbst, zum Beispiel<Buffer 42 c3 a4 63 6b ...>. Text wird es erst mit der Kodierung. - Es gibt auch eine Variante, bei der das Programm nicht anhält und auf die Festplatte wartet:
import { readFile } from "node:fs/promises";undconst text = await readFile("expenses.txt", "utf8");. Server verwenden diese. In kurzen Programmen istreadFileSynceinfacher. readFileSyncliest die ganze Datei in den Speicher. Bei einer Datei von mehreren Gigabyte liest du besser Zeile für Zeile. Siehe die Karte über Streams.
Probier es aus
Zwei Schichten
Was gibt das Programm aus?
import { readFileSync, writeFileSync } from "node:fs";
try { writeFileSync("shift.txt", "Erste Schicht: Jonas\n"); writeFileSync("shift.txt", "Zweite Schicht: Maria\n"); process.stdout.write(readFileSync("shift.txt", "utf8"));} catch (e) { console.log(e.message);}writeFileSync löscht den alten Inhalt und schreibt den neuen. Die erste Zeile ist schon vor dem Lesen weg. Das ist der häufigste Grund für „meine Datei wurde gelöscht“.
Die Idee
Wenn die Datei nicht da ist
Die Einstellungen des Programms stehen in config.txt, und jemand hat sie gelöscht. Wenn niemand die Exception fängt:
import { readFileSync } from "node:fs";
function readConfig() { return readFileSync("config.txt", "utf8");}
console.log(readConfig());node:fs:440 return binding.readFileUtf8(path, stringToFlags(options.flag)); ^
Error: ENOENT: no such file or directory, open 'config.txt' at readFileSync (node:fs:440:20) at readConfig (file:///app/app.js:4:10) at file:///app/app.js:7:13 ... { errno: -2, code: 'ENOENT', syscall: 'open', path: 'config.txt'}Ganz oben zeigt Node die Zeile aus seinem eigenen inneren Code, in der der Fehler geworfen wurde. Dann kommt der Fehler selbst: ENOENT (error, no entry) ist der Code für „keine solche Datei und kein solcher Ordner“. Die Zeilen mit „at“ gehen von innen nach außen: readFileSync, dann readConfig in Zeile 4, aufgerufen aus Zeile 7. Die Zeilen von Node danach sind mit „...“ gekürzt.
Die Fehler von node:fs sind von der Art Error und unterscheiden sich durch e.code. Prüf den Code im catch:
import { existsSync, readFileSync } from "node:fs";
function readConfig() { return readFileSync("config.txt", "utf8");}
try { console.log(readConfig());} catch (e) { if (e.code === "ENOENT") { console.log(`Keine Datei ${e.path}. Ich nehme die Standardeinstellungen.`); } else { console.log(`Kann die Einstellungen nicht lesen: ${e.message}`); }}console.log(existsSync("config.txt"));Keine Datei config.txt. Ich nehme die Standardeinstellungen.falsee.path ist der Name der fehlenden Datei. existsSync(path) prüft, ob die Datei existiert. Die Prüfung ersetzt das catch nicht: Die Datei kann zwischen der Prüfung und dem Lesen verschwinden, und es gibt noch andere Gründe, warum das Lesen scheitern kann.
Die Idee
Zeile für Zeile und Streams
writeFileSync schreibt den ganzen Text auf einmal. Zum Schreiben in Teilen und zum Lesen Zeile für Zeile gibt es Streams. Ein Stream hält die Datei offen, solange du mit ihr arbeitest, und muss deshalb am Ende geschlossen werden:
import { createReadStream, createWriteStream } from "node:fs";import { once } from "node:events";import { createInterface } from "node:readline";
try { const out = createWriteStream("report.txt"); for (let day = 1; day <= 3; day++) { out.write(`Tag ${day}: ${day * 120} €\n`); } out.end(); await once(out, "close");} catch (e) { console.log(`Kann nicht schreiben: ${e.message}`);}
try { const lines = createInterface({ input: createReadStream("report.txt") }); for await (const line of lines) { console.log(line); }} catch (e) { console.log(`Kann nicht lesen: ${e.message}`);}Tag 1: 120 €Tag 2: 240 €Tag 3: 360 €createWriteStreamöffnet die Datei zum Schreiben.writegibt den Text an den Stream. Ein Zeilenumbruch kommt nicht von selbst: Das\nschreibst du.end()sagt „das ist alles“ und beginnt das Schließen.await once(out, "close")wartet, bis der Stream alles geschrieben und die Datei geschlossen hat.onceist aus dem Modulnode:events.createInterfaceausnode:readlineliest den Stream aus der Datei Zeile für Zeile, undfor awaitgibt die Zeilen eine nach der anderen, ohne das Zeichen für den Zeilenumbruch. Damit liest du auch von der Tastatur.
Das Schließen ist keine Formalität: Solange der Stream nicht geschlossen ist, kann der Text noch in seinem Puffer im Speicher stehen und nicht auf der Festplatte. Damit der Code kurz bleibt, gibt es hier kein try:
import { createWriteStream, readFileSync, statSync } from "node:fs";import { once } from "node:events";
const out = createWriteStream("report.txt");for (let day = 1; day <= 3; day++) { out.write(`Tag ${day}: ${day * 120} €\n`);}console.log(`Im Puffer: ${out.writableLength} Bytes`);out.end();await once(out, "close");console.log(`In der Datei: ${statSync("report.txt").size} Bytes`);for (const line of readFileSync("report.txt", "utf8").trimEnd().split("\n")) { console.log(line);}Schritt für Schritt, in Worten
- node:fs liefert den Stream und die Größe der Datei, node:events das Warten. Links ist der Speicher des Programms, rechts die Festplatte.
- createWriteStream legt einen Stream mit einem Puffer im Speicher an. Die Datei report.txt wird geöffnet, ohne dass das Programm wartet.
- write gibt den Text an den Stream, und er wartet im Puffer. Auf der Festplatte kommt noch nichts an.
- writableLength: 45 Bytes warten im Puffer. Noch keines davon ist auf der Festplatte.
- end() schließt den Stream, und await wartet, bis das passiert ist. Der Puffer geht auf die Festplatte: Die Datei hat jetzt 45 Bytes.
- 45 Bytes für 39 Zeichen mit den Zeilenumbrüchen: In UTF-8 ist jeder lateinische Buchstabe 1 Byte, und € sind 3.
- readFileSync liest die drei Zeilen von der Festplatte zurück.
JavaScript hat kein try (...), das eine Ressource von selbst schließt. readFileSync und writeFileSync brauchen keins, weil sie die Datei selbst öffnen und schließen. Wenn du die Datei offen hältst, steht das Schließen in finally, aus der Lektion über finally:
import { closeSync, openSync, writeSync } from "node:fs";
const fd = openSync("report.txt", "w");try { writeSync(fd, "Tag 1: 120 €\n");} finally { closeSync(fd);}openSync öffnet die Datei und gibt ihre Nummer zurück (fd, File-Deskriptor). writeSync schreibt direkt in die Datei, ohne Puffer. closeSync schließt sie, egal was im try passiert.
Genauer, für Neugierige
- Die Sprache bekommt
using: eine Ressource, die sich am Ende des Blocks von selbst schließt, wietry (...)in Java. Node 22 erkennt es noch nicht und meldet einenSyntaxError. - Puffer. Der Stream sammelt den Text im Speicher (
out.writableLengthist, wie viele Bytes warten) und schickt ihn auf die Festplatte, ohne dass das Programm anhält und wartet. Deshalb brauchst du nachend()einawait. readlineübercreateReadStreamist der Weg für eine sehr große Datei: Nur das aktuelle Stück ist im Speicher. Hier ist es für sich allein:
import { createReadStream, writeFileSync } from "node:fs";import { createInterface } from "node:readline";
writeFileSync("report.txt", "Tag 1: 120 €\nTag 2: 240 €\n");const lines = createInterface({ input: createReadStream("report.txt") });for await (const line of lines) { console.log(line);}Probier es aus
Wann wird geschlossen?
JavaScript hat kein try (...), deshalb wird die Ressource in finally geschlossen. Hier ist der Kassendrucker aus der Lektion über finally: close() steht im finally des inneren try, und der Fehler wird vom äußeren gefangen:
Was gibt das Programm aus?
class Printer { constructor() { console.log("Der Drucker ist belegt."); }
close() { console.log("Der Drucker ist frei."); }}
try { const printer = new Printer(); try { console.log("Drucke den Kassenbon..."); const qty = Number("drei"); if (Number.isNaN(qty)) { throw new Error("drei ist keine Zahl"); } console.log(`Menge: ${qty}`); } finally { printer.close(); }} catch (e) { console.log("Falsche Menge.");}Number("drei") ergibt NaN ohne Fehler, deshalb wirft die Prüfung selbst. Die Zeile „Menge“ wird übersprungen. Das finally des inneren try läuft sofort, noch vor dem äußeren catch. Deshalb kommt „frei“ vor „Falsche Menge.“ Wenn catch läuft, ist der Drucker schon freigegeben.Probier es aus
Finde den Fehler
1 aus, aber es sind zwei Zeilen. In welcher Zeile ist der Fehler?Wähle die Zeile mit dem Fehler.
end() beginnt nur das Schließen. Das Schreiben passiert später, während readFileSync sofort liest, wenn nur die Kopfzeile in der Datei steht. Das Programm meldet keinen Fehler. Es zählt still falsch. Füge nach out.end(); die Zeile await once(out, "close"); ein (once ist aus node:events): Dann ist der Stream geschlossen, und es sind zwei Zeilen.Die Idee
Ordner
Die Sicherungskopien der Kasse kommen in einen Ordner, eine pro Tag:
import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";import { join } from "node:path";
const backup = "backup";try { mkdirSync(join(backup, "old"), { recursive: true }); writeFileSync(join(backup, "2026-10-01.csv"), "Brötchen,0.60\n"); writeFileSync(join(backup, "2026-10-02.csv"), "Brötchen,0.60\nTee,1.80\n"); for (const name of readdirSync(backup).sort()) { const p = join(backup, name); if (statSync(p).isDirectory()) { console.log(`${name}/`); } else { console.log(`${name} (${statSync(p).size} Bytes)`); } } unlinkSync(join(backup, "2026-10-01.csv")); console.log(existsSync(join(backup, "2026-10-01.csv")));} catch (e) { console.log(e.message);}2026-10-01.csv (15 Bytes)2026-10-02.csv (24 Bytes)old/false| Funktion | Was sie tut |
|---|---|
mkdirSync(p, { recursive: true }) |
legt den Ordner und alle Ordner darüber an, wenn es sie nicht gibt |
join(dir, "name") (aus node:path) |
der Pfad zu name in dir |
statSync(p).isDirectory(), existsSync(p) |
ist es ein Ordner, ist es da |
statSync(p).size |
die Größe in Bytes |
unlinkSync(p) |
löscht eine Datei (rmdirSync(p) löscht einen leeren Ordner) |
readdirSync(dir) |
ein Array mit den Namen von allem im Ordner |
Die Größen sind 15 und 24 Bytes, obwohl es weniger Zeichen sind: In UTF-8 ist das „ö“ in „Brötchen“ 2 Bytes.
readdirSync verspricht keine Reihenfolge der Namen, deshalb sortieren wir sie mit sort().Genauer, für Neugierige
rmdirSynclöscht nur einen leeren Ordner. Sonst wirft es einen Fehler mit dem CodeENOTEMPTY.rmSync(p, { recursive: true })löscht den Ordner mit allem darin, also sei vorsichtig damit.copyFileSync(src, dst)kopiert eine Datei, undrenameSync(src, dst)verschiebt oder benennt sie um.joinsetzt das Trennzeichen des Systems zwischen die Teile:/unter Linux und macOS,\unter Windows. Deshalb baust du Pfade nicht mit+zusammen.- Der Pfad
"notes.txt"ist relativ: zu dem Ordner, aus dem das Programm gestartet wurde."/home/maria/notes.txt"ist absolut.resolve("notes.txt")ausnode:pathzeigt genau, wo die Datei ist.
Übung
Programme, die sich erinnern
Löse die Aufgaben in deinem Editor. In jeder Aufgabe legt das Programm die Eingabedatei zuerst selbst an, mit writeFileSync, damit du es sofort ausprobieren kannst.
1. Eine Konfigurationsdatei. Lege server.conf mit diesem Inhalt an, lies sie dann und gib jede Einstellung als Schlüssel: Wert aus. Überspringe die leeren Zeilen und die Zeilen, die mit # beginnen. Tipp: Finde das erste = mit indexOf und nimm die beiden Teile mit slice. split("=", 2) hilft hier nicht: In JavaScript wirft es alles nach dem zweiten = weg.
# Servereinstellungenmax_connections=500log_file_path=/var/log/server.log
db_timeout=30max_connections: 500log_file_path: /var/log/server.logdb_timeout: 302. Ein Filter für eine Logdatei. Lege app.log mit vier Zeilen an (zwei INFO und zwei ERROR). Lies ein Level von der Tastatur, zum Beispiel ERROR, und schreib die Zeilen mit diesem Level in errors.log Zeile für Zeile: lies mit readline, schreib mit einem Stream (createWriteStream). Gib errors.log aus und wie viele Zeilen du geschrieben hast.
ERRORERROR: Failed to load configurationERROR: Null pointer exception2 Zeilen in errors.log geschrieben3. Der Inhalt eines Ordners. Lege einen Ordner project mit den Dateien document.txt, image.jpg, script.js und dem Ordner backup an. Schreib ihre Namen, alphabetisch sortiert, in directory_contents.txt, mit einem / nach den Namen von Ordnern. Gib die Datei aus.
backup/document.txtimage.jpgscript.js4. Eine Indexdatei. Lege text.txt mit den drei Zeilen unten an. Lies ein Wort von der Tastatur und schreib in index.txt jede Stelle, an der es vorkommt, in der Form Zeile:Position: die Nummer der Zeile (ab 1) und den Index in der Zeile (ab 0). Wenn das Wort zweimal in einer Zeile steht, sind das zwei Einträge.
hello worldworld hellohello again, hellohello1:02:63:03:13Weitere Aufgaben
- Zähler für die Starts. Repariere das Programm aus der ersten Karte: Zähl die Starts in
visits.txt. Wenn es die Datei nicht gibt, fang bei 0 an. - Suchen und Ersetzen in einer Datei. Die Aufgabe aus der Lektion „Reguläre Ausdrücke: Gruppen und Ersetzen“: Lies eine Datei, ersetze „alt“ durch „neu“ und schreib das Ergebnis in eine neue Datei.
- Datumsangaben aus einer Datei. Die Datumsaufgabe aus der Lektion „Reguläre Ausdrücke“: Lies die Datumsangaben aus einer Datei, eine pro Zeile.
- Eine Kopie mit Zeilennummern. Lies eine Datei und schreib eine Kopie, in der jede Zeile mit ihrer Nummer beginnt:
1: ...,2: ....
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Interfaces ohne interface · Exceptions · try, catch und finally · Strings
- Geht weiter in: Daten in Dateien · Hashcodes und Hashfunktionen · Netzwerkprogrammierung · Datenbanken
- Aufgaben zum Thema: Text und Dateien