Zum Inhalt springen
aviral gupta

// I3.5 · ca. 45 Min. · Aufbau

Projekt: ein To-do-Werkzeug für die Kommandozeile

In diesem Projekt schreiben Sie ein To-do-Werkzeug, das Sie als node main.js add Buy milk starten: drei Module, eine JSON-Datei, die die Liste behält, und klare Fehler.

Lektion 5 von 5 in I3 Module, Node.js und npm

Ende des Moduls

Danach können Sie

  • Befehle und Optionen mit parseArgs aus node:util von der Kommandozeile lesen
  • Daten zwischen Läufen in einer JSON-Datei halten und mit einer leeren Liste starten, wenn die Datei fehlt
  • Falsche Eingaben auf stderr mit einem Exit-Code ungleich 0 melden und die Logik in einem testbaren Modul halten
  1. Aufwärmen · Aufgabe 1 von 7

    Zum Aufwärmen aus Lektion I3.1. Was gibt node main.js aus?

    // todos.js
    export function count(todos) {
      return todos.length;
    }
    
    // main.js
    import {count} from "./todos.js";
    console.log(count(["a", "b"]));
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen. Sie tippen node main.js add "Buy milk" in die Shell. Was gibt main.js aus?

    // main.js
    console.log(process.argv.slice(2));
  3. Üben · Aufgabe 3 von 7

    --file nimmt einen Wert, etwa work.json. Ergänzen Sie seinen Typ.

    options: {file: {type: "____", default: "todos.json"}},
    type: "", default
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jede Aufgabe dem Modul des Werkzeugs zu, das sie erledigt.

  5. Üben · Aufgabe 5 von 7

    Die Liste wird als JSON gespeichert und zurückgelesen. Was gibt node main.js aus?

    // main.js
    const saved = JSON.stringify([{id: 1, text: "Tea", done: false, added: new Date(0)}]);
    const [todo] = JSON.parse(saved);
    console.log(typeof todo.id, typeof todo.done, typeof todo.added);
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Was erscheint auf stdout, und mit welchem Exit-Code endet das Programm?

    // main.js
    console.error("No to-do with id 7");
    process.exitCode = 1;
    console.log("bye");
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe: Legen Sie das Werkzeug in einen Ordner mit package.json ("type": "module") und ergänzen Sie zwei Skripte, "todo": "node main.js" und "test": "node --test". Schreiben Sie todos.test.js mit zwei Tests für todos.js: Die IDs zählen nach einem Entfernen weiter, und eine unbekannte ID wirft. Starten Sie npm test, dann npm run todo -- add Buy milk und npm run todo -- list.

    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

Die zwei inneren Module bei der Arbeit

Vor der Kommandozeile sehen Sie todos.js und store.js in einem kurzen Skript. Es lädt eine Liste aus einer Datei, die es noch nicht gibt, fügt zwei To-dos hinzu, markiert eines als erledigt, speichert die Liste und liest sie so zurück, wie es ein späterer Lauf täte. In den Übungen bauen Sie main.js, die Kommandozeile um die beiden herum.

main.js

import {addTodo, markDone, formatList} from "./todos.js";
import {loadTodos, saveTodos} from "./store.js";

const file = "example.json";
let todos = await loadTodos(file); // [] the first time: no file yet
console.log(formatList(todos));

todos = addTodo(todos, "Buy milk");
todos = addTodo(todos, "Call Ada");
todos = markDone(todos, 1);
await saveTodos(file, todos);

// A later run would start here: read the file again.
console.log(formatList(await loadTodos(file)));

try {
  markDone(todos, 7);
} catch (error) {
  console.log(error.message);
}

todos.js

// A to-do is {id, text, done}. Every function returns a new array.
export function addTodo(todos, text) {
  const id = todos.length === 0 ? 1 : Math.max(...todos.map((todo) => todo.id)) + 1;
  return [...todos, {id, text, done: false}];
}

export function markDone(todos, id) {
  if (!todos.some((todo) => todo.id === id)) throw new Error(`No to-do with id ${id}`);
  return todos.map((todo) => (todo.id === id ? {...todo, done: true} : todo));
}

export function removeTodo(todos, id) {
  if (!todos.some((todo) => todo.id === id)) throw new Error(`No to-do with id ${id}`);
  return todos.filter((todo) => todo.id !== id);
}

export function formatList(todos) {
  if (todos.length === 0) return "Nothing to do.";
  return todos.map((todo) => `${todo.done ? "[x]" : "[ ]"} ${todo.id}. ${todo.text}`).join("\n");
}

store.js

import {readFile, writeFile} from "node:fs/promises";

// The list in the file, or an empty list if the file does not exist yet.
export async function loadTodos(file) {
  try {
    return JSON.parse(await readFile(file, "utf8"));
  } catch (error) {
    if (error.code === "ENOENT") return [];
    throw error;
  }
}

export async function saveTodos(file, todos) {
  await writeFile(file, JSON.stringify(todos, null, 2) + "\n");
}

Ausführen mit

node main.js

Ausgabe

Nothing to do.
[x] 1. Buy milk
[ ] 2. Call Ada
No to-do with id 7
  • addTodo und markDone geben neue Arrays zurück; die alte Liste wird nie geändert.
  • example.json enthält die Liste jetzt als eingerücktes JSON, Sie können sie also in einem Editor lesen.
  • markDone wirft bei einer unbekannten ID; main.js macht daraus eine Meldung auf stderr und den Exit-Code 1.

Übungen

Übung 1 von 3

Add mit jedem Wort

main.js ist die Kommandozeile des Werkzeugs, mit todos.js und store.js daneben. node main.js add Buy milk soll das To-do "Buy milk" hinzufügen, aber der Startcode behält nur das erste Wort. Korrigieren Sie die Zeile, die den Text baut. Die Tests nutzen ihre eigene --file, sie beginnen also mit einer leeren Liste.

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

    words enthält jedes Wort nach add. Welches davon nutzt der Startcode?

  2. Hinweis 2

    Verbinden Sie die Wörter mit einem Leerzeichen.

  3. Hinweis 3

    addTodo(todos, words.join(" "))

Eine Lösung zeigen

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

import {parseArgs} from "node:util";
import {addTodo, markDone, removeTodo, formatList} from "./todos.js";
import {loadTodos, saveTodos} from "./store.js";

const USAGE = "Usage: node main.js [--file todos.json] add <text> | list | done <id> | remove <id>";

const {values, positionals} = parseArgs({
  options: {file: {type: "string", default: "todos.json"}},
  allowPositionals: true
});
const [command, ...words] = positionals;
const todos = await loadTodos(values.file);

try {
  if (command === "add" && words.length > 0) {
    const next = addTodo(todos, words.join(" "));
    await saveTodos(values.file, next);
    console.log(`Added ${next.at(-1).id}. ${next.at(-1).text}`);
  } else if (command === "list") {
    console.log(formatList(todos));
  } else if (command === "done" || command === "remove") {
    const id = Number(words[0]);
    const next = command === "done" ? markDone(todos, id) : removeTodo(todos, id);
    await saveTodos(values.file, next);
    console.log(`${command === "done" ? "Done" : "Removed"}: ${id}`);
  } else {
    console.error(USAGE);
    process.exitCode = 1;
  }
} catch (error) {
  console.error(error.message);
  process.exitCode = 1;
}
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 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 {parseArgs} from "node:util";
import {addTodo, markDone, removeTodo, formatList} from "./todos.js";
import {loadTodos, saveTodos} from "./store.js";

const USAGE = "Usage: node main.js [--file todos.json] add <text> | list | done <id> | remove <id>";

const {values, positionals} = parseArgs({
  options: {file: {type: "string", default: "todos.json"}},
  allowPositionals: true
});
const [command, ...words] = positionals;
const todos = await loadTodos(values.file);

try {
  if (command === "add" && words.length > 0) {
    const next = addTodo(todos, words[0]);
    await saveTodos(values.file, next);
    console.log(`Added ${next.at(-1).id}. ${next.at(-1).text}`);
  } else if (command === "list") {
    console.log(formatList(todos));
  } else if (command === "done" || command === "remove") {
    const id = Number(words[0]);
    const next = command === "done" ? markDone(todos, id) : removeTodo(todos, id);
    await saveTodos(values.file, next);
    console.log(`${command === "done" ? "Done" : "Removed"}: ${id}`);
  } else {
    console.error(USAGE);
    process.exitCode = 1;
  }
} catch (error) {
  console.error(error.message);
  process.exitCode = 1;
}

main.test.js

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

const file = 'test-' + Date.now() + '-' + Math.floor(Math.random() * 1e6) + '.json';
const todo = (...args) => runMain({args: ['--file', file, ...args]});

test('add Buy milk fügt ein To-do mit beiden Wörtern hinzu', async () => {
  const got = await todo('add', 'Buy', 'milk');
  assert.equal(got, 'Added 1. Buy milk\n', `add gab ${JSON.stringify(got)} aus`);
});

test('list zeigt es, noch nicht erledigt', async () => {
  const got = await todo('list');
  assert.equal(got, '[ ] 1. Buy milk\n', `list gab ${JSON.stringify(got)} aus`);
});

todos.js

// A to-do is {id, text, done}. Every function returns a new array.
export function addTodo(todos, text) {
  const id = todos.length === 0 ? 1 : Math.max(...todos.map((todo) => todo.id)) + 1;
  return [...todos, {id, text, done: false}];
}

export function markDone(todos, id) {
  if (!todos.some((todo) => todo.id === id)) throw new Error(`No to-do with id ${id}`);
  return todos.map((todo) => (todo.id === id ? {...todo, done: true} : todo));
}

export function removeTodo(todos, id) {
  if (!todos.some((todo) => todo.id === id)) throw new Error(`No to-do with id ${id}`);
  return todos.filter((todo) => todo.id !== id);
}

export function formatList(todos) {
  if (todos.length === 0) return "Nothing to do.";
  return todos.map((todo) => `${todo.done ? "[x]" : "[ ]"} ${todo.id}. ${todo.text}`).join("\n");
}

store.js

import {readFile, writeFile} from "node:fs/promises";

// The list in the file, or an empty list if the file does not exist yet.
export async function loadTodos(file) {
  try {
    return JSON.parse(await readFile(file, "utf8"));
  } catch (error) {
    if (error.code === "ENOENT") return [];
    throw error;
  }
}

export async function saveTodos(file, todos) {
  await writeFile(file, JSON.stringify(todos, null, 2) + "\n");
}

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 3

Eine ID von der Kommandozeile

node main.js done 1 soll To-do 1 als erledigt markieren, aber der Startcode antwortet No to-do with id 1. Die ID kommt als Text von der Kommandozeile, und die IDs in der Liste sind Zahlen. Korrigieren Sie die Zeile, die die ID liest. Eine unbekannte ID muss weiterhin mit einem Exit-Code ungleich 0 enden.

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

    Was ist typeof words[0]? Und womit vergleicht markDone es?

  2. Hinweis 2

    === zwischen dem Text "1" und der Zahl 1 ist false.

  3. Hinweis 3

    const id = Number(words[0]);

Eine Lösung zeigen

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

import {parseArgs} from "node:util";
import {addTodo, markDone, removeTodo, formatList} from "./todos.js";
import {loadTodos, saveTodos} from "./store.js";

const USAGE = "Usage: node main.js [--file todos.json] add <text> | list | done <id> | remove <id>";

const {values, positionals} = parseArgs({
  options: {file: {type: "string", default: "todos.json"}},
  allowPositionals: true
});
const [command, ...words] = positionals;
const todos = await loadTodos(values.file);

try {
  if (command === "add" && words.length > 0) {
    const next = addTodo(todos, words.join(" "));
    await saveTodos(values.file, next);
    console.log(`Added ${next.at(-1).id}. ${next.at(-1).text}`);
  } else if (command === "list") {
    console.log(formatList(todos));
  } else if (command === "done" || command === "remove") {
    const id = Number(words[0]);
    const next = command === "done" ? markDone(todos, id) : removeTodo(todos, id);
    await saveTodos(values.file, next);
    console.log(`${command === "done" ? "Done" : "Removed"}: ${id}`);
  } else {
    console.error(USAGE);
    process.exitCode = 1;
  }
} catch (error) {
  console.error(error.message);
  process.exitCode = 1;
}
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 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 {parseArgs} from "node:util";
import {addTodo, markDone, removeTodo, formatList} from "./todos.js";
import {loadTodos, saveTodos} from "./store.js";

const USAGE = "Usage: node main.js [--file todos.json] add <text> | list | done <id> | remove <id>";

const {values, positionals} = parseArgs({
  options: {file: {type: "string", default: "todos.json"}},
  allowPositionals: true
});
const [command, ...words] = positionals;
const todos = await loadTodos(values.file);

try {
  if (command === "add" && words.length > 0) {
    const next = addTodo(todos, words.join(" "));
    await saveTodos(values.file, next);
    console.log(`Added ${next.at(-1).id}. ${next.at(-1).text}`);
  } else if (command === "list") {
    console.log(formatList(todos));
  } else if (command === "done" || command === "remove") {
    const id = words[0];
    const next = command === "done" ? markDone(todos, id) : removeTodo(todos, id);
    await saveTodos(values.file, next);
    console.log(`${command === "done" ? "Done" : "Removed"}: ${id}`);
  } else {
    console.error(USAGE);
    process.exitCode = 1;
  }
} catch (error) {
  console.error(error.message);
  process.exitCode = 1;
}

main.test.js

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

const file = 'test-' + Date.now() + '-' + Math.floor(Math.random() * 1e6) + '.json';
const todo = (...args) => runMain({args: ['--file', file, ...args]});

test('done 2 markiert das zweite To-do', async () => {
  await todo('add', 'Tea');
  await todo('add', 'Bread');
  const got = await todo('done', '2');
  assert.equal(got, 'Done: 2\n', `done 2 gab ${JSON.stringify(got)} aus`);
});

test('list zeigt nur das zweite als erledigt', async () => {
  const got = await todo('list');
  assert.equal(got, '[ ] 1. Tea\n[x] 2. Bread\n', `list gab ${JSON.stringify(got)} aus`);
});

test('remove 1 entfernt das erste', async () => {
  await todo('remove', '1');
  const got = await todo('list');
  assert.equal(got, '[x] 2. Bread\n', `nach remove 1 gab list ${JSON.stringify(got)} aus`);
});

test('done 9 endet mit einem Exit-Code ungleich 0', async () => {
  await assert.rejects(todo('done', '9'), Error, 'done 9 endete mit Exit-Code 0');
});

todos.js

// A to-do is {id, text, done}. Every function returns a new array.
export function addTodo(todos, text) {
  const id = todos.length === 0 ? 1 : Math.max(...todos.map((todo) => todo.id)) + 1;
  return [...todos, {id, text, done: false}];
}

export function markDone(todos, id) {
  if (!todos.some((todo) => todo.id === id)) throw new Error(`No to-do with id ${id}`);
  return todos.map((todo) => (todo.id === id ? {...todo, done: true} : todo));
}

export function removeTodo(todos, id) {
  if (!todos.some((todo) => todo.id === id)) throw new Error(`No to-do with id ${id}`);
  return todos.filter((todo) => todo.id !== id);
}

export function formatList(todos) {
  if (todos.length === 0) return "Nothing to do.";
  return todos.map((todo) => `${todo.done ? "[x]" : "[ ]"} ${todo.id}. ${todo.text}`).join("\n");
}

store.js

import {readFile, writeFile} from "node:fs/promises";

// The list in the file, or an empty list if the file does not exist yet.
export async function loadTodos(file) {
  try {
    return JSON.parse(await readFile(file, "utf8"));
  } catch (error) {
    if (error.code === "ENOENT") return [];
    throw error;
  }
}

export async function saveTodos(file, todos) {
  await writeFile(file, JSON.stringify(todos, null, 2) + "\n");
}

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

Falsche Eingaben scheitern laut

Ohne Befehl, bei einem unbekannten Befehl oder bei add ohne Wörter soll das Werkzeug die Usage-Zeile auf stderr ausgeben und mit Exit-Code 1 enden, damit ein aufrufendes Skript es merkt. Der Startcode gibt die Usage-Zeile mit console.log aus, und der Exit-Code bleibt 0. Korrigieren Sie den else-Zweig.

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

    Zwei Dinge: wohin die Usage-Zeile geht und der Exit-Code.

  2. Hinweis 2

    console.error schreibt auf stderr.

  3. Hinweis 3

    console.error(USAGE); dann process.exitCode = 1;

Eine Lösung zeigen

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

import {parseArgs} from "node:util";
import {addTodo, markDone, removeTodo, formatList} from "./todos.js";
import {loadTodos, saveTodos} from "./store.js";

const USAGE = "Usage: node main.js [--file todos.json] add <text> | list | done <id> | remove <id>";

const {values, positionals} = parseArgs({
  options: {file: {type: "string", default: "todos.json"}},
  allowPositionals: true
});
const [command, ...words] = positionals;
const todos = await loadTodos(values.file);

try {
  if (command === "add" && words.length > 0) {
    const next = addTodo(todos, words.join(" "));
    await saveTodos(values.file, next);
    console.log(`Added ${next.at(-1).id}. ${next.at(-1).text}`);
  } else if (command === "list") {
    console.log(formatList(todos));
  } else if (command === "done" || command === "remove") {
    const id = Number(words[0]);
    const next = command === "done" ? markDone(todos, id) : removeTodo(todos, id);
    await saveTodos(values.file, next);
    console.log(`${command === "done" ? "Done" : "Removed"}: ${id}`);
  } else {
    console.error(USAGE);
    process.exitCode = 1;
  }
} catch (error) {
  console.error(error.message);
  process.exitCode = 1;
}
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 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 {parseArgs} from "node:util";
import {addTodo, markDone, removeTodo, formatList} from "./todos.js";
import {loadTodos, saveTodos} from "./store.js";

const USAGE = "Usage: node main.js [--file todos.json] add <text> | list | done <id> | remove <id>";

const {values, positionals} = parseArgs({
  options: {file: {type: "string", default: "todos.json"}},
  allowPositionals: true
});
const [command, ...words] = positionals;
const todos = await loadTodos(values.file);

try {
  if (command === "add" && words.length > 0) {
    const next = addTodo(todos, words.join(" "));
    await saveTodos(values.file, next);
    console.log(`Added ${next.at(-1).id}. ${next.at(-1).text}`);
  } else if (command === "list") {
    console.log(formatList(todos));
  } else if (command === "done" || command === "remove") {
    const id = Number(words[0]);
    const next = command === "done" ? markDone(todos, id) : removeTodo(todos, id);
    await saveTodos(values.file, next);
    console.log(`${command === "done" ? "Done" : "Removed"}: ${id}`);
  } else {
    console.log(USAGE);
  }
} catch (error) {
  console.error(error.message);
  process.exitCode = 1;
}

main.test.js

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

const file = 'test-' + Date.now() + '-' + Math.floor(Math.random() * 1e6) + '.json';
const todo = (...args) => runMain({args: ['--file', file, ...args]});

test('kein Befehl endet mit einem Exit-Code ungleich 0', async () => {
  await assert.rejects(todo(), Error, 'node main.js ohne Befehl endete mit Exit-Code 0');
});

test('add ohne Wörter endet mit einem Exit-Code ungleich 0', async () => {
  await assert.rejects(todo('add'), Error, 'add ohne Wörter endete mit Exit-Code 0');
});

test('ein unbekannter Befehl endet mit einem Exit-Code ungleich 0', async () => {
  await assert.rejects(todo('fly'), Error, 'der Befehl fly endete mit Exit-Code 0');
});

test('list funktioniert weiterhin und gibt Nothing to do. aus', async () => {
  const got = await todo('list');
  assert.equal(got, 'Nothing to do.\n', `list gab ${JSON.stringify(got)} aus`);
});

todos.js

// A to-do is {id, text, done}. Every function returns a new array.
export function addTodo(todos, text) {
  const id = todos.length === 0 ? 1 : Math.max(...todos.map((todo) => todo.id)) + 1;
  return [...todos, {id, text, done: false}];
}

export function markDone(todos, id) {
  if (!todos.some((todo) => todo.id === id)) throw new Error(`No to-do with id ${id}`);
  return todos.map((todo) => (todo.id === id ? {...todo, done: true} : todo));
}

export function removeTodo(todos, id) {
  if (!todos.some((todo) => todo.id === id)) throw new Error(`No to-do with id ${id}`);
  return todos.filter((todo) => todo.id !== id);
}

export function formatList(todos) {
  if (todos.length === 0) return "Nothing to do.";
  return todos.map((todo) => `${todo.done ? "[x]" : "[ ]"} ${todo.id}. ${todo.text}`).join("\n");
}

store.js

import {readFile, writeFile} from "node:fs/promises";

// The list in the file, or an empty list if the file does not exist yet.
export async function loadTodos(file) {
  try {
    return JSON.parse(await readFile(file, "utf8"));
  } catch (error) {
    if (error.code === "ENOENT") return [];
    throw error;
  }
}

export async function saveTodos(file, todos) {
  await writeFile(file, JSON.stringify(todos, null, 2) + "\n");
}

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

Eine Datei lesen, die es noch nicht gibt

import {readFile} from "node:fs/promises";

const todos = JSON.parse(await readFile("todos.json", "utf8"));
console.log(todos.length);

Was Node.js ausgibt

Error: ENOENT: no such file or directory, open 'todos.json'

Warum, und die Lösung

Beim ersten Lauf gibt es noch keine Datei. Fangen Sie den Fehler ab und geben Sie eine leere Liste zurück, wenn error.code "ENOENT" ist; werfen Sie jeden anderen Fehler weiter, damit kaputtes JSON nicht stillschweigend durch eine leere Liste ersetzt wird. loadTodos aus store.js tut genau das.

Eine leere Datei ist kein gültiges JSON

import {readFile, writeFile} from "node:fs/promises";

await writeFile("todos.json", "");
const todos = JSON.parse(await readFile("todos.json", "utf8"));
console.log(todos.length);

Was Node.js ausgibt

SyntaxError: Unexpected end of JSON input

Warum, und die Lösung

JSON.parse braucht einen vollständigen JSON-Wert; ein leerer Text ist keiner. Schreiben Sie die Liste immer mit JSON.stringify, damit die Datei mindestens [] enthält. Wenn Menschen die Datei von Hand bearbeiten, fangen Sie den SyntaxError ab und sagen Sie, welche Datei kaputt ist, statt mit einer leeren Liste zu starten.

Eine Option, die parseArgs nicht kennt

import {parseArgs} from "node:util";

const {values} = parseArgs({
  args: ["--fil", "work.json"],
  options: {file: {type: "string"}}
});
console.log(values.file);

Was Node.js ausgibt

TypeError [ERR_PARSE_ARGS_UNKNOWN_OPTION]: Unknown option '--fil'

Warum, und die Lösung

parseArgs ist standardmäßig streng: Jede Option muss unter options deklariert sein, ein Tippfehler scheitert also sofort, statt ignoriert zu werden. Hier ist --fil ein Tippfehler für --file. Fangen Sie diesen Fehler in main.js ab und geben Sie die Usage-Zeile aus, oder setzen Sie strict: false, wenn Sie unbekannte Optionen wirklich akzeptieren wollen.

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

Drei Module, drei Aufgaben

todos.js enthält reine Funktionen: Sie nehmen die Liste und geben eine neue zurück und berühren nie Dateien oder die Konsole, ein Test kann sie also direkt aufrufen. store.js liest und schreibt die Liste als JSON-Datei. main.js liest die Kommandozeile, ruft die beiden anderen auf und gibt das Ergebnis aus. Ändert sich etwas, wissen Sie, welche Datei Sie öffnen: Ein neuer Befehl kommt in main.js, ein neues Dateiformat in store.js.

Befehle und Optionen

process.argv enthält jedes Wort der Kommandozeile; die ersten zwei Einträge sind der Pfad von node und von main.js. parseArgs aus node:util sortiert den Rest: Optionen, die Sie deklarieren, etwa --file mit type "string" und einem Default, landen in values; mit allowPositionals: true landen die übrigen Wörter in positionals, etwa ["add", "Buy", "milk"]. Eine nicht deklarierte Option wirft einen TypeError mit dem Code ERR_PARSE_ARGS_UNKNOWN_OPTION.

Eine Datei, die es noch nicht gibt

Beim ersten Lauf gibt es keine todos.json. readFile wird dann mit einem Fehler abgelehnt, dessen code "ENOENT" ist, und loadTodos gibt für genau diesen Code eine leere Liste zurück; jeder andere Fehler, etwa kaputtes JSON, wird weitergeworfen. saveTodos schreibt JSON.stringify(todos, null, 2), das sich gut lesen und vergleichen lässt. Falsche Eingaben gehen an console.error, das auf stderr schreibt, und process.exitCode = 1 teilt der Shell mit, dass der Befehl gescheitert ist, während das Programm ganz normal endet.

Quellen

Zuletzt geprüft am 5. Oktober 2026