Zum Inhalt springen
aviral gupta

// B3.3 · ca. 34 Min. · Einstieg

Schreiben, Anhängen und atomares Ersetzen

Nach dieser Lektion schreiben Sie Dateien, hängen an, verhindern Überschreiben, speichern lesbares JSON und ersetzen eine Datei so, dass sie nie halb geschrieben ist.

Lektion 3 von 5 in B3 Dateien und Pfade

Danach können Sie

  • Eine Datei mit writeFile ersetzen, mit appendFile ergänzen und mit dem Flag wx Überschreiben verweigern
  • Einen Wert als lesbares JSON speichern, in einem Ordner, den mkdir anlegt, wenn er fehlt
  • Eine Datei atomar ersetzen: eine Temp-Datei im selben Ordner schreiben und sie dann über das Ziel umbenennen
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus B3.2: readFile("missing.txt", "utf8") wird abgelehnt, weil es die Datei nicht gibt. Welchen error.code trägt der Fehler?

  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen. notes.txt enthält die Zeile buy milk. Was gibt dieses Programm aus?

    import {readFile, writeFile} from 'node:fs/promises';
    
    await writeFile('notes.txt', 'call Ada\n');
    console.log(await readFile('notes.txt', 'utf8'));
  3. Üben · Aufgabe 3 von 7

    app.log enthält schon started. Setzen Sie die Funktion ein, damit stopped als zweite Zeile dazukommt und started bleibt.

    await ____("app.log", "stopped\n");
    await ("app.log", "stopped\n");
  4. Üben · Aufgabe 4 von 7

    f.txt enthält old. Ordnen Sie jedem Flag von writeFile("f.txt", "new\n", {flag}) zu, was passiert.

  5. Üben · Aufgabe 5 von 7

    Setzen Sie das dritte Argument ein, damit settings.json pro Ebene um zwei Leerzeichen eingerückt ist und mit einem Zeilenumbruch endet.

    await writeFile("settings.json", JSON.stringify(settings, null, ____) + "\n");
    await writeFile("settings.json", JSON.stringify(settings, null, ) + "\n");
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. settings.json enthält {"theme": "light"}. Das Programm unten speichert die neuen Einstellungen über eine Temp-Datei, stirbt aber vor dem rename. Was enthält settings.json danach?

    import {rename, writeFile} from 'node:fs/promises';
    
    await writeFile('.settings.json.tmp', '{"theme": "dark"}\n');
    throw new Error('power cut'); // the program dies here
    await rename('.settings.json.tmp', 'settings.json');
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Schreiben Sie main.js so, dass node main.js "buy milk" die Notiz in data/notes.json neben main.js ablegt, ein JSON-Array mit zwei Leerzeichen Einrückung. Fehlen Ordner oder Datei, beginnt einfach eine leere Liste. Ersetzen Sie notes.json über eine Temp-Datei und rename, hängen Sie eine Zeile added: buy milk an data/notes.log an und geben Sie aus, wie viele Notizen es gibt. Führen Sie es zweimal aus und öffnen Sie beide Dateien.

    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 Log, eine Weigerung und eine Einstellungsdatei

Dieses Programm legt den Ordner out neben main.js an und schreibt dort ein Log: writeFile beginnt es, appendFile fügt zwei Zeilen an. Ein zweites writeFile mit dem Flag wx weigert sich, das Log zu überschreiben, und meldet EEXIST. Zuletzt speichert es Einstellungen als eingerücktes JSON auf die sichere Art, über eine Temp-Datei und rename, und listet den Ordner: Keine Temp-Datei bleibt übrig. Führen Sie es zweimal aus, bleibt die Ausgabe gleich, weil writeFile das Log neu beginnt.

main.js

import {appendFile, mkdir, readFile, readdir, rename, writeFile} from 'node:fs/promises';
import {join} from 'node:path';

const dir = join(import.meta.dirname, 'out');
await mkdir(dir, {recursive: true}); // fine if out already exists

// writeFile replaces, appendFile adds.
const log = join(dir, 'app.log');
await writeFile(log, 'started\n');
await appendFile(log, 'saved settings\n');
await appendFile(log, 'stopped\n');
console.log((await readFile(log, 'utf8')).trimEnd());

// 'wx' refuses to overwrite an existing file.
try {
  await writeFile(log, 'oops\n', {flag: 'wx'});
} catch (error) {
  console.log('wx:', error.code);
}

// JSON, replaced in one step: temp file in the same folder, then rename.
const settings = join(dir, 'settings.json');
const temp = join(dir, `.settings.json.${process.pid}.tmp`);
await writeFile(temp, JSON.stringify({theme: 'dark', fontSize: 14}, null, 2) + '\n');
await rename(temp, settings);
console.log((await readFile(settings, 'utf8')).trimEnd());
console.log('in out:', (await readdir(dir)).sort().join(', '));

Ausführen mit

node main.js

Ausgabe

started
saved settings
stopped
wx: EEXIST
{
  "theme": "dark",
  "fontSize": 14
}
in out: app.log, settings.json
  • appendFile hat zwei Zeilen nach started angefügt, weil jedes Datenstück mit "\n" endet.
  • Mit {flag: 'wx'} lehnt writeFile mit EEXIST ab, und app.log behält seine drei Zeilen.
  • JSON.stringify(value, null, 2) hat jede Eigenschaft in eine eigene Zeile gesetzt, zwei Leerzeichen tief.
  • Die Temp-Datei wurde über settings.json umbenannt, also enthält der Ordner nur die zwei echten Dateien.

Übungen

Übung 1 von 2

Ein Temp-Name neben dem Ziel

Vor einem atomaren Ersetzen brauchen Sie einen Temp-Pfad im selben Ordner wie das Ziel. Schreiben Sie tempPathFor(target, id): Setzen Sie einen Punkt vor den Dateinamen und .<id>.tmp dahinter, und lassen Sie den Ordnerteil unverändert. "data/notes.json" mit id 42 ergibt "data/.notes.json.42.tmp"; "notes.json" ergibt ".notes.json.42.tmp". Pfade verwenden hier /. Dieser Teil ist reine String-Arbeit, er läuft also auch im Browser.

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

    target.lastIndexOf("/") findet den letzten Schrägstrich; ohne einen ist es -1.

  2. Hinweis 2

    Plus 1 ergibt den Index, an dem der Dateiname beginnt, auch 0, wenn es keinen Schrägstrich gibt.

  3. Hinweis 3

    target.slice(0, cut) ist der Ordnerteil mit Schrägstrich, target.slice(cut) der Dateiname.

Eine Lösung zeigen

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

// The temp path for an atomic replace: same folder, a dot before the name,
// .<id>.tmp after it. "data/notes.json", 42 -> "data/.notes.json.42.tmp"
export function tempPathFor(target, id) {
  const cut = target.lastIndexOf("/") + 1;
  return target.slice(0, cut) + "." + target.slice(cut) + "." + id + ".tmp";
}

console.log(tempPathFor("data/notes.json", 42));
Auf dem eigenen Computer ausführen

Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

// The temp path for an atomic replace: same folder, a dot before the name,
// .<id>.tmp after it. "data/notes.json", 42 -> "data/.notes.json.42.tmp"
export function tempPathFor(target, id) {
  return target + ".tmp";
}

console.log(tempPathFor("data/notes.json", 42));

main.test.js

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

test('eine Datei in einem Ordner behält ihren Ordner', () => {
  const got = tempPathFor('data/notes.json', 42);
  assert.equal(got, 'data/.notes.json.42.tmp', `tempPathFor lieferte ${JSON.stringify(got)}`);
});

test('eine Datei ohne Ordner', () => {
  const got = tempPathFor('notes.json', 7);
  assert.equal(got, '.notes.json.7.tmp', `tempPathFor lieferte ${JSON.stringify(got)}`);
});

test('ein tiefer absoluter Pfad', () => {
  const got = tempPathFor('/home/ada/app/data/notes.json', 1234);
  assert.equal(got, '/home/ada/app/data/.notes.json.1234.tmp', `tempPathFor lieferte ${JSON.stringify(got)}`);
});

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

saveJson, sicher

saveJson(path, value) schreibt value direkt nach path und scheitert, weil der Ordner data noch nicht existiert. Machen Sie es sicher: Legen Sie den Ordner von path mit mkdir und {recursive: true} an, schreiben Sie JSON.stringify(value, null, 2) plus "\n" in eine Temp-Datei in diesem Ordner, etwa .notes.json.<process.pid>.tmp, und benennen Sie die Temp-Datei dann über path um. Der Code darunter speichert zweimal und gibt Datei und Ordner aus. Starten Sie node main.js und prüfen Sie mit node --test.

Diese Übung braucht Node.js auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.

Hinweise
  1. Hinweis 1

    dirname(path) ist der Ordner; mkdir(dirname(path), {recursive: true}) tut nichts, wenn er schon existiert.

  2. Hinweis 2

    Bilden Sie den Temp-Pfad im selben Ordner: join(dir, `.${basename(path)}.${process.pid}.tmp`).

  3. Hinweis 3

    writeFile(temp, JSON.stringify(value, null, 2) + "\n"), dann rename(temp, path).

Eine Lösung zeigen

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

import {mkdir, readFile, readdir, rename, writeFile} from 'node:fs/promises';
import {basename, dirname, join} from 'node:path';

// Saves value as indented JSON, replacing path in one step.
export async function saveJson(path, value) {
  const dir = dirname(path);
  await mkdir(dir, {recursive: true});
  const temp = join(dir, `.${basename(path)}.${process.pid}.tmp`);
  await writeFile(temp, JSON.stringify(value, null, 2) + '\n');
  await rename(temp, path);
}

const file = join(import.meta.dirname, 'data', 'notes.json');
await saveJson(file, {notes: ['buy milk']});
await saveJson(file, {notes: ['buy milk', 'call Ada']});
console.log((await readFile(file, 'utf8')).trimEnd());
console.log('ends with a newline:', (await readFile(file, 'utf8')).endsWith('\n'));
console.log('files in data:', (await readdir(dirname(file))).sort().join(', '));
Auf dem eigenen Computer ausführen

Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

import {mkdir, readFile, readdir, rename, writeFile} from 'node:fs/promises';
import {basename, dirname, join} from 'node:path';

// Saves value as indented JSON, replacing path in one step.
export async function saveJson(path, value) {
  await writeFile(path, JSON.stringify(value));
}

const file = join(import.meta.dirname, 'data', 'notes.json');
await saveJson(file, {notes: ['buy milk']});
await saveJson(file, {notes: ['buy milk', 'call Ada']});
console.log((await readFile(file, 'utf8')).trimEnd());
console.log('ends with a newline:', (await readFile(file, 'utf8')).endsWith('\n'));
console.log('files in data:', (await readdir(dirname(file))).sort().join(', '));

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';

test('läuft und legt den Ordner data an', async () => {
  await runMain();
});

test('notes.json enthält das zweite Speichern, um zwei Leerzeichen eingerückt', async () => {
  const got = (await runMain()).split('\n').slice(0, 6).join('\n');
  const expected = '{\n  "notes": [\n    "buy milk",\n    "call Ada"\n  ]\n}';
  assert.equal(got, expected, `die Datei beginnt mit ${JSON.stringify(got)}`);
});

test('notes.json endet mit einem Zeilenumbruch', async () => {
  const out = await runMain();
  assert.match(out, /ends with a newline: true/, 'die Datei endet nicht mit "\\n"');
});

test('in data bleibt keine Temp-Datei übrig', async () => {
  const last = (await runMain()).trimEnd().split('\n').at(-1);
  assert.equal(last, 'files in data: notes.json', `der Ordner enthält: ${last}`);
});

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

Ein Objekt statt Text schreiben

import {writeFile} from 'node:fs/promises';

await writeFile('settings.json', {theme: 'dark'});

Was Node.js ausgibt

TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Object

Warum, und die Lösung

writeFile schreibt Text oder Bytes, und seit v14 wandelt es andere Werte nicht mehr für Sie in Strings um. Machen Sie aus dem Wert zuerst JSON: writeFile("settings.json", JSON.stringify(settings, null, 2) + "\n").

In einen Ordner schreiben, den es nicht gibt

import {writeFile} from 'node:fs/promises';

await writeFile('data/notes.json', '[]\n');

Was Node.js ausgibt

Error: ENOENT: no such file or directory, open 'data/notes.json'

Warum, und die Lösung

writeFile legt die Datei an, aber nicht die Ordner auf dem Weg dorthin. Legen Sie zuerst den Ordner an: await mkdir("data", {recursive: true}). Mit recursive entsteht jeder fehlende übergeordnete Ordner, und es scheitert nicht, wenn der Ordner schon da ist.

Auf den Ordner statt auf die Datei umbenennen

import {mkdir, rename, writeFile} from 'node:fs/promises';

await mkdir('data', {recursive: true});
await writeFile('.notes.json.tmp', '[]\n');
await rename('.notes.json.tmp', 'data'); // meant data/notes.json

Was Node.js ausgibt

Error: EISDIR: illegal operation on a directory, rename '.notes.json.tmp' -> 'data'

Warum, und die Lösung

rename erwartet den vollständigen neuen Pfad der Datei, nicht den Ordner, in dem sie landen soll. rename überschreibt eine vorhandene Datei, aber liegt am neuen Pfad ein Ordner, gibt es stattdessen einen Fehler. Schreiben Sie rename(".notes.json.tmp", "data/notes.json").

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

Ersetzen, anhängen oder verweigern

writeFile(path, data) aus node:fs/promises legt die Datei an oder ersetzt ihren gesamten Inhalt: Das Standard-Flag 'w' leert eine vorhandene Datei. appendFile(path, data) hängt data am Ende an und legt die Datei an, wenn sie fehlt; sein Flag ist 'a'. Keine der beiden fügt einen Zeilenumbruch hinzu, also beenden Sie jede Zeile selbst mit "\n". Die Option flag ändert, was writeFile tut: {flag: 'a'} hängt an, und {flag: 'wx'} legt die Datei an, scheitert aber mit EEXIST, wenn der Pfad schon existiert, sodass Sie nie versehentlich etwas überschreiben. Existiert der Ordner im Pfad nicht, scheitert das Schreiben mit ENOENT.

JSON auf der Platte, und ein Ordner dafür

writeFile nimmt einen String oder Bytes, kein Objekt: Seit v14 wandelt es andere Werte nicht mehr in Strings um, und {theme: 'dark'} zu übergeben wirft einen TypeError. Machen Sie aus dem Wert zuerst Text. JSON.stringify(value, null, 2) rückt um zwei Leerzeichen ein; das macht die Datei lesbar und ihre Änderungen leicht vergleichbar. Hängen Sie "\n" an, damit die Datei mit einem Zeilenumbruch endet, wie Textdateien es meist tun. Kann der Ordner fehlen, legen Sie ihn zuerst mit mkdir(dir, {recursive: true}) an: Es erzeugt jeden fehlenden übergeordneten Ordner und scheitert nicht, wenn der Ordner schon existiert. B3.4 behandelt Ordner ausführlich.

Atomar ersetzen: Temp-Datei, dann rename

writeFile leert die Datei zuerst und schreibt dann. Stirbt das Programm dazwischen oder liest ein anderes Programm genau dann, findet es eine leere oder halb geschriebene Datei. Das sichere Muster: Schreiben Sie den vollständigen neuen Text in eine Temp-Datei im selben Ordner, etwa .settings.json.123.tmp, und rufen Sie dann rename(temp, target) auf. Laut fs-Doku überschreibt rename den neuen Pfad, wenn er schon existiert; die alte Datei wird also in einem Schritt ersetzt. Bis dahin hat das Ziel noch seinen alten, vollständigen Inhalt. Wählen Sie einen Temp-Namen, den kein anderer Lauf verwendet, etwa mit process.pid, und benennen Sie nie auf einen Ordner um: Das ergibt EISDIR.

Quellen

Zuletzt geprüft am 3. Oktober 2026