Zum Inhalt
wannacode

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.`);
Ausgabe
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:

notes.js
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}`);
}
Ausgabe
Milch kaufen
Stromrechnung bis zum 15. bezahlen
  • import { readFileSync, writeFileSync } from "node:fs"; nimmt zwei Funktionen aus dem Modul node: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 catch hält das Programm an.
  • process.stdout.write(text) gibt den Text so aus, wie er ist. console.log wü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}`);
}
Ausgabe
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
  • appendFileSync legt die Datei an, wenn es sie nicht gibt, deshalb gibt das erste Anhängen keinen Fehler.
  • Ohne "utf8" gibt readFileSync einen Buffer zurü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"; und const text = await readFile("expenses.txt", "utf8");. Server verwenden diese. In kurzen Programmen ist readFileSync einfacher.
  • readFileSync liest 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);
}
Antwort

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:

app.js
import { readFileSync } from "node:fs";
function readConfig() {
return readFileSync("config.txt", "utf8");
}
console.log(readConfig());
Fehler
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"));
Ausgabe
Keine Datei config.txt. Ich nehme die Standardeinstellungen.
false

e.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}`);
}
Ausgabe
Tag 1: 120 €
Tag 2: 240 €
Tag 3: 360 €
  • createWriteStream öffnet die Datei zum Schreiben. write gibt den Text an den Stream. Ein Zeilenumbruch kommt nicht von selbst: Das \n schreibst 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. once ist aus dem Modul node:events.
  • createInterface aus node:readline liest den Stream aus der Datei Zeile für Zeile, und for await gibt 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
  1. node:fs liefert den Stream und die Größe der Datei, node:events das Warten. Links ist der Speicher des Programms, rechts die Festplatte.
  2. createWriteStream legt einen Stream mit einem Puffer im Speicher an. Die Datei report.txt wird geöffnet, ohne dass das Programm wartet.
  3. write gibt den Text an den Stream, und er wartet im Puffer. Auf der Festplatte kommt noch nichts an.
  4. writableLength: 45 Bytes warten im Puffer. Noch keines davon ist auf der Festplatte.
  5. end() schließt den Stream, und await wartet, bis das passiert ist. Der Puffer geht auf die Festplatte: Die Datei hat jetzt 45 Bytes.
  6. 45 Bytes für 39 Zeichen mit den Zeilenumbrüchen: In UTF-8 ist jeder lateinische Buchstabe 1 Byte, und € sind 3.
  7. 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, wie try (...) in Java. Node 22 erkennt es noch nicht und meldet einen SyntaxError.
  • Puffer. Der Stream sammelt den Text im Speicher (out.writableLength ist, wie viele Bytes warten) und schickt ihn auf die Festplatte, ohne dass das Programm anhält und wartet. Deshalb brauchst du nach end() ein await.
  • readline über createReadStream ist 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.");
}
Antwort

Probier es aus

Finde den Fehler

Das Programm hängt eine Bestellung unter die Kopfzeile an und zählt die Zeilen in der Datei. Es gibt 1 aus, aber es sind zwei Zeilen. In welcher Zeile ist der Fehler?

Wähle die Zeile mit dem Fehler.

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);
}
Ausgabe
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
  • rmdirSync löscht nur einen leeren Ordner. Sonst wirft es einen Fehler mit dem Code ENOTEMPTY. rmSync(p, { recursive: true }) löscht den Ordner mit allem darin, also sei vorsichtig damit.
  • copyFileSync(src, dst) kopiert eine Datei, und renameSync(src, dst) verschiebt oder benennt sie um.
  • join setzt 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") aus node:path zeigt 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.

server.conf
# Servereinstellungen
max_connections=500
log_file_path=/var/log/server.log
db_timeout=30
Ausgabe
max_connections: 500
log_file_path: /var/log/server.log
db_timeout: 30

2. 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.

Eingabe
ERROR
Ausgabe
ERROR: Failed to load configuration
ERROR: Null pointer exception
2 Zeilen in errors.log geschrieben

3. 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.

directory_contents.txt
backup/
document.txt
image.jpg
script.js

4. 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.

text.txt
hello world
world hello
hello again, hello
Eingabe
hello
index.txt
1:0
2:6
3:0
3:13

Weitere 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

1. Was gibt readFileSync("notes.txt") ohne "utf8" zurück?
2. writeFileSync(file, "A\n"); danach writeFileSync(file, "B\n"); Was steht in der Datei?
3. Hat JavaScript (Node 22) ein try (...), das die Datei von selbst schließt?
4. readFileSync findet die Datei nicht. Woran erkennst du, dass genau das das Problem ist?
5. Du schreibst mit createWriteStream, rufst end() auf und liest die Datei sofort mit readFileSync. Was kann darin stehen?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·