Zum Inhalt springen
aviral gupta

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

Projekt: eine getestete Validierungsbibliothek

In diesem Projekt schreiben Sie validate.js, eine Bibliothek, die Formulardaten gegen Regeln prüft, alle Probleme auf einmal meldet und ihre eigenen Tests mitbringt.

Lektion 5 von 5 in I4 Fehler, Debugging und Tests

Ende des Moduls

Danach können Sie

  • Regeln als kleine Funktionen schreiben und eine verletzte Regel als ValidationError mit ihrem Feld melden
  • Alle Probleme in einem AggregateError sammeln und einen JSON-SyntaxError als cause behalten
  • Die Bibliothek mit node:test testen: welcher Fehler geworfen wird, seine Felder und seine Meldungen
  1. Aufwärmen · Aufgabe 1 von 7

    Zum Aufwärmen, aus Lektion I4.2. Was gibt dieser Code aus?

    class ValidationError extends Error {
      constructor(field, message) {
        super(`${field}: ${message}`);
        this.name = "ValidationError";
        this.field = field;
      }
    }
    const error = new ValidationError("age", "is required");
    console.log(String(error), "|", error.field);
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen: Diese erste Version von validate wirft beim ersten Problem. Das Formular hat drei. Was gibt sie aus?

    import {validate, required, minLength, integer} from "./validate.js";
    
    const schema = {name: [minLength(2)], age: [integer(16, 120)], city: [required()]};
    try {
      validate(schema, {name: "A", age: 15, city: ""});
    } catch (error) {
      console.log(error.message);
    }
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie die Fehlerklasse, die ein ganzes Array von Fehlern trägt.

    throw new ____(errors, "2 invalid fields");
    throw new (errors, "2 invalid fields");
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jedem Teil der Bibliothek seine Aufgabe zu.

  5. Üben · Aufgabe 5 von 7

    minLength(2) gibt eine Prüfung zurück. Was gibt dieser Code aus?

    import {minLength} from "./validate.js";
    
    const atLeast2 = minLength(2);
    console.log(atLeast2("Al"), "|", atLeast2("A"));
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Das Alter kommt aus einem Formularfeld, so wie der Browser es sendet. Was gibt dieser Code aus?

    import {validate, integer} from "./validate.js";
    
    const form = {age: "36"};
    try {
      validate({age: [integer(16, 120)]}, form);
      console.log("ok");
    } catch (error) {
      console.log(error.errors[0].message);
    }
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe: Ergänzen Sie validate.js um eine Regel maxLength(max), mit dem Problemtext must have at most <max> characters. Schreiben Sie dann validate.test.js mit zwei Tests: Ein kurzer Name besteht und kommt unverändert zurück, und ein langer Name wirft einen AggregateError, dessen erster Fehler name: must have at most 5 characters sagt. Führen Sie node --test aus.

    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 fertige Bibliothek an drei Formularen

main.js validiert drei Anmeldeformulare mit validate.js gegen ein Schema. Das erste ist in Ordnung, das zweite verletzt in jedem Feld eine Regel, das dritte hat keinen Namen. Jedes Problem wird aufgelistet, mit einer Meldung pro Feld.

main.js

import {validate, required, minLength, integer, oneOf} from "./validate.js";

const signup = {
  name: [required(), minLength(2)],
  age: [required(), integer(16, 120)],
  plan: [required(), oneOf(["free", "team"])]
};

const forms = [
  {name: "Ada", age: 36, plan: "free"},
  {name: "A", age: 15.5, plan: "gold"},
  {age: 30, plan: "team"}
];

for (const form of forms) {
  try {
    validate(signup, form);
    console.log("ok:", form.name);
  } catch (error) {
    if (!(error instanceof AggregateError)) throw error;
    console.log(error.message);
    for (const problem of error.errors) console.log("  " + problem.message);
  }
}

validate.js

export class ValidationError extends Error {
  constructor(field, message, options) {
    super(`${field}: ${message}`, options);
    this.name = "ValidationError";
    this.field = field;
  }
}

// Each rule returns a check: a function that gets a value and returns
// the problem as text, or null when the value is fine.
export const required = () => (value) =>
  value === undefined || value === null || value === "" ? "is required" : null;

export const minLength = (min) => (value) =>
  typeof value === "string" && value.length >= min ? null : `must have at least ${min} characters`;

export const integer = (min, max) => (value) =>
  Number.isInteger(value) && value >= min && value <= max ? null : `must be a whole number from ${min} to ${max}`;

export const oneOf = (choices) => (value) =>
  choices.includes(value) ? null : `must be one of ${choices.join(", ")}`;

export function validate(schema, data) {
  const errors = [];
  for (const [field, checks] of Object.entries(schema)) {
    for (const check of checks) {
      const problem = check(data[field]);
      if (problem !== null) {
        errors.push(new ValidationError(field, problem));
        break; // one problem per field is enough
      }
    }
  }
  if (errors.length > 0) {
    throw new AggregateError(errors, `${errors.length} invalid field${errors.length === 1 ? "" : "s"}`);
  }
  return data;
}

export function parseBody(text, schema) {
  let data;
  try {
    data = JSON.parse(text);
  } catch (error) {
    throw new ValidationError("body", "is not valid JSON", {cause: error});
  }
  if (typeof data !== "object" || data === null || Array.isArray(data)) {
    throw new ValidationError("body", "must be a JSON object");
  }
  return validate(schema, data);
}

Ausführen mit

node main.js

Ausgabe

ok: Ada
3 invalid fields
  name: must have at least 2 characters
  age: must be a whole number from 16 to 120
  plan: must be one of free, team
1 invalid field
  name: is required
  • Dem dritten Formular fehlt name, und required() hält dort an: Es sagt nicht zusätzlich "must have at least 2 characters".
  • main.js behandelt nur AggregateError; jeder andere Fehler wird weitergeworfen, weil er ein Fehler im Code wäre.
  • Das Schema sind einfache Daten, ein neues Formular braucht also ein neues Schema, nicht ein neues validate.
Ändern und ausführen

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.

Übungen

Übung 1 von 3

Alle Probleme auf einmal

main.js enthält die Bibliothek, validate.js aus dem Beispiel. Ihr validate hält bei der ersten verletzten Regel an und wirft deren ValidationError. Ändern Sie es so, dass es pro verletztem Feld einen ValidationError sammelt (nur das erste Problem jedes Feldes) und dann einen AggregateError mit der Meldung <n> invalid field wirft, bei mehr als einem Feld <n> invalid fields. Gültige Daten werden unverändert zurückgegeben.

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

    Beginnen Sie mit const errors = [] und fügen Sie einen neuen ValidationError mit push hinzu, statt ihn zu werfen.

  2. Hinweis 2

    Verlassen Sie nach dem push die innere Schleife mit break, damit ein Feld nur sein erstes Problem meldet.

  3. Hinweis 3

    Werfen Sie nach beiden Schleifen new AggregateError(errors, message), wenn errors nicht leer ist.

Eine Lösung zeigen

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

export class ValidationError extends Error {
  constructor(field, message, options) {
    super(`${field}: ${message}`, options);
    this.name = "ValidationError";
    this.field = field;
  }
}

// Each rule returns a check: a function that gets a value and returns
// the problem as text, or null when the value is fine.
export const required = () => (value) =>
  value === undefined || value === null || value === "" ? "is required" : null;

export const minLength = (min) => (value) =>
  typeof value === "string" && value.length >= min ? null : `must have at least ${min} characters`;

export const integer = (min, max) => (value) =>
  Number.isInteger(value) && value >= min && value <= max ? null : `must be a whole number from ${min} to ${max}`;

export const oneOf = (choices) => (value) =>
  choices.includes(value) ? null : `must be one of ${choices.join(", ")}`;

export function validate(schema, data) {
  const errors = [];
  for (const [field, checks] of Object.entries(schema)) {
    for (const check of checks) {
      const problem = check(data[field]);
      if (problem !== null) {
        errors.push(new ValidationError(field, problem));
        break; // one problem per field is enough
      }
    }
  }
  if (errors.length > 0) {
    throw new AggregateError(errors, `${errors.length} invalid field${errors.length === 1 ? "" : "s"}`);
  }
  return data;
}

export function parseBody(text, schema) {
  let data;
  try {
    data = JSON.parse(text);
  } catch (error) {
    throw new ValidationError("body", "is not valid JSON", {cause: error});
  }
  if (typeof data !== "object" || data === null || Array.isArray(data)) {
    throw new ValidationError("body", "must be a JSON object");
  }
  return validate(schema, data);
}
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

export class ValidationError extends Error {
  constructor(field, message, options) {
    super(`${field}: ${message}`, options);
    this.name = "ValidationError";
    this.field = field;
  }
}

// Each rule returns a check: a function that gets a value and returns
// the problem as text, or null when the value is fine.
export const required = () => (value) =>
  value === undefined || value === null || value === "" ? "is required" : null;

export const minLength = (min) => (value) =>
  typeof value === "string" && value.length >= min ? null : `must have at least ${min} characters`;

export const integer = (min, max) => (value) =>
  Number.isInteger(value) && value >= min && value <= max ? null : `must be a whole number from ${min} to ${max}`;

export const oneOf = (choices) => (value) =>
  choices.includes(value) ? null : `must be one of ${choices.join(", ")}`;

export function validate(schema, data) {
  for (const [field, checks] of Object.entries(schema)) {
    for (const check of checks) {
      const problem = check(data[field]);
      if (problem !== null) throw new ValidationError(field, problem);
    }
  }
  return data;
}

export function parseBody(text, schema) {
  let data;
  try {
    data = JSON.parse(text);
  } catch (error) {
    throw new ValidationError("body", "is not valid JSON", {cause: error});
  }
  if (typeof data !== "object" || data === null || Array.isArray(data)) {
    throw new ValidationError("body", "must be a JSON object");
  }
  return validate(schema, data);
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {validate, required, minLength, integer} from './main.js';

/** The error that fn throws; the test fails when fn throws nothing. */
function caught(fn) {
  try {
    fn();
  } catch (error) {
    return error;
  }
  assert.fail('es wurde nichts geworfen');
}

const schema = {name: [required(), minLength(2)], age: [required(), integer(16, 120)]};

test('gültige Daten kommen unverändert zurück', () => {
  assert.deepEqual(validate(schema, {name: 'Ada', age: 36}), {name: 'Ada', age: 36}, 'validate sollte die Daten zurückgeben');
});

test('zwei verletzte Felder ergeben einen AggregateError mit zwei Fehlern', () => {
  const error = caught(() => validate(schema, {name: 'A', age: 15}));
  assert.ok(error instanceof AggregateError, `validate warf ${error.name}, keinen AggregateError`);
  assert.equal(error.message, '2 invalid fields');
  assert.deepEqual(error.errors.map((e) => e.field), ['name', 'age']);
});

test('ein Problem pro Feld, und der Singular bei einem Feld', () => {
  const error = caught(() => validate(schema, {age: 30}));
  assert.ok(error instanceof AggregateError, `validate warf ${error.name}, keinen AggregateError`);
  assert.equal(error.message, '1 invalid field');
  assert.deepEqual(error.errors.map((e) => e.message), ['name: is required']);
});

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 eigene Regel: oneOf

main.js enthält die Bibliothek, validate.js aus dem Beispiel. oneOf(choices) soll eine Prüfung zurückgeben, die nur die aufgelisteten Werte akzeptiert und sonst must be one of gefolgt von den Auswahlwerten zurückgibt, getrennt durch Komma und Leerzeichen: must be one of free, team. Im Starter akzeptiert die Prüfung alles.

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

    Sehen Sie sich minLength an: eine Funktion, die eine Funktion von value zurückgibt.

  2. Hinweis 2

    choices.includes(value) sagt, ob der Wert aufgeführt ist.

  3. Hinweis 3

    choices.join(", ") baut die Liste für die Meldung.

Eine Lösung zeigen

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

export class ValidationError extends Error {
  constructor(field, message, options) {
    super(`${field}: ${message}`, options);
    this.name = "ValidationError";
    this.field = field;
  }
}

// Each rule returns a check: a function that gets a value and returns
// the problem as text, or null when the value is fine.
export const required = () => (value) =>
  value === undefined || value === null || value === "" ? "is required" : null;

export const minLength = (min) => (value) =>
  typeof value === "string" && value.length >= min ? null : `must have at least ${min} characters`;

export const integer = (min, max) => (value) =>
  Number.isInteger(value) && value >= min && value <= max ? null : `must be a whole number from ${min} to ${max}`;

export const oneOf = (choices) => (value) =>
  choices.includes(value) ? null : `must be one of ${choices.join(", ")}`;

export function validate(schema, data) {
  const errors = [];
  for (const [field, checks] of Object.entries(schema)) {
    for (const check of checks) {
      const problem = check(data[field]);
      if (problem !== null) {
        errors.push(new ValidationError(field, problem));
        break; // one problem per field is enough
      }
    }
  }
  if (errors.length > 0) {
    throw new AggregateError(errors, `${errors.length} invalid field${errors.length === 1 ? "" : "s"}`);
  }
  return data;
}

export function parseBody(text, schema) {
  let data;
  try {
    data = JSON.parse(text);
  } catch (error) {
    throw new ValidationError("body", "is not valid JSON", {cause: error});
  }
  if (typeof data !== "object" || data === null || Array.isArray(data)) {
    throw new ValidationError("body", "must be a JSON object");
  }
  return validate(schema, data);
}
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

export class ValidationError extends Error {
  constructor(field, message, options) {
    super(`${field}: ${message}`, options);
    this.name = "ValidationError";
    this.field = field;
  }
}

// Each rule returns a check: a function that gets a value and returns
// the problem as text, or null when the value is fine.
export const required = () => (value) =>
  value === undefined || value === null || value === "" ? "is required" : null;

export const minLength = (min) => (value) =>
  typeof value === "string" && value.length >= min ? null : `must have at least ${min} characters`;

export const integer = (min, max) => (value) =>
  Number.isInteger(value) && value >= min && value <= max ? null : `must be a whole number from ${min} to ${max}`;

export const oneOf = (choices) => (value) => null; // TODO

export function validate(schema, data) {
  const errors = [];
  for (const [field, checks] of Object.entries(schema)) {
    for (const check of checks) {
      const problem = check(data[field]);
      if (problem !== null) {
        errors.push(new ValidationError(field, problem));
        break; // one problem per field is enough
      }
    }
  }
  if (errors.length > 0) {
    throw new AggregateError(errors, `${errors.length} invalid field${errors.length === 1 ? "" : "s"}`);
  }
  return data;
}

export function parseBody(text, schema) {
  let data;
  try {
    data = JSON.parse(text);
  } catch (error) {
    throw new ValidationError("body", "is not valid JSON", {cause: error});
  }
  if (typeof data !== "object" || data === null || Array.isArray(data)) {
    throw new ValidationError("body", "must be a JSON object");
  }
  return validate(schema, data);
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {validate, oneOf} from './main.js';

/** The error that fn throws; the test fails when fn throws nothing. */
function caught(fn) {
  try {
    fn();
  } catch (error) {
    return error;
  }
  assert.fail('es wurde nichts geworfen');
}

test('ein aufgeführter Wert ist in Ordnung', () => {
  assert.equal(oneOf(['free', 'team'])('team'), null, 'team gehört zu den Auswahlwerten');
});

test('ein anderer Wert nennt die Auswahlwerte', () => {
  assert.equal(oneOf(['free', 'team'])('gold'), 'must be one of free, team');
});

test('validate meldet das Feld', () => {
  const error = caught(() => validate({plan: [oneOf(['free', 'team'])]}, {plan: 'gold'}));
  assert.deepEqual(error.errors?.map((e) => e.message), ['plan: must be one of free, team']);
});

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

Kaputtes JSON ist ein ValidationError

main.js enthält die Bibliothek, validate.js aus dem Beispiel. parseBody(text, schema) liest einen Request-Body als JSON und validiert ihn. Kaputtes JSON entkommt bisher als nackter SyntaxError, und der Body null lässt validate abstürzen. Sorgen Sie dafür, dass parseBody einen ValidationError für das Feld body wirft: is not valid JSON, mit dem SyntaxError als cause, und must be a JSON object, wenn das JSON kein Objekt ist (null, ein Array, eine Zahl oder ein String).

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

    Setzen Sie JSON.parse in try und werfen Sie in catch den neuen ValidationError, mit {cause: error}.

  2. Hinweis 2

    typeof null ist ebenfalls "object", prüfen Sie data === null also gesondert.

  3. Hinweis 3

    Array.isArray(data) unterscheidet ein Array von einem Objekt.

Eine Lösung zeigen

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

export class ValidationError extends Error {
  constructor(field, message, options) {
    super(`${field}: ${message}`, options);
    this.name = "ValidationError";
    this.field = field;
  }
}

// Each rule returns a check: a function that gets a value and returns
// the problem as text, or null when the value is fine.
export const required = () => (value) =>
  value === undefined || value === null || value === "" ? "is required" : null;

export const minLength = (min) => (value) =>
  typeof value === "string" && value.length >= min ? null : `must have at least ${min} characters`;

export const integer = (min, max) => (value) =>
  Number.isInteger(value) && value >= min && value <= max ? null : `must be a whole number from ${min} to ${max}`;

export const oneOf = (choices) => (value) =>
  choices.includes(value) ? null : `must be one of ${choices.join(", ")}`;

export function validate(schema, data) {
  const errors = [];
  for (const [field, checks] of Object.entries(schema)) {
    for (const check of checks) {
      const problem = check(data[field]);
      if (problem !== null) {
        errors.push(new ValidationError(field, problem));
        break; // one problem per field is enough
      }
    }
  }
  if (errors.length > 0) {
    throw new AggregateError(errors, `${errors.length} invalid field${errors.length === 1 ? "" : "s"}`);
  }
  return data;
}

export function parseBody(text, schema) {
  let data;
  try {
    data = JSON.parse(text);
  } catch (error) {
    throw new ValidationError("body", "is not valid JSON", {cause: error});
  }
  if (typeof data !== "object" || data === null || Array.isArray(data)) {
    throw new ValidationError("body", "must be a JSON object");
  }
  return validate(schema, data);
}
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

export class ValidationError extends Error {
  constructor(field, message, options) {
    super(`${field}: ${message}`, options);
    this.name = "ValidationError";
    this.field = field;
  }
}

// Each rule returns a check: a function that gets a value and returns
// the problem as text, or null when the value is fine.
export const required = () => (value) =>
  value === undefined || value === null || value === "" ? "is required" : null;

export const minLength = (min) => (value) =>
  typeof value === "string" && value.length >= min ? null : `must have at least ${min} characters`;

export const integer = (min, max) => (value) =>
  Number.isInteger(value) && value >= min && value <= max ? null : `must be a whole number from ${min} to ${max}`;

export const oneOf = (choices) => (value) =>
  choices.includes(value) ? null : `must be one of ${choices.join(", ")}`;

export function validate(schema, data) {
  const errors = [];
  for (const [field, checks] of Object.entries(schema)) {
    for (const check of checks) {
      const problem = check(data[field]);
      if (problem !== null) {
        errors.push(new ValidationError(field, problem));
        break; // one problem per field is enough
      }
    }
  }
  if (errors.length > 0) {
    throw new AggregateError(errors, `${errors.length} invalid field${errors.length === 1 ? "" : "s"}`);
  }
  return data;
}

export function parseBody(text, schema) {
  return validate(schema, JSON.parse(text));
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {parseBody, ValidationError, required} from './main.js';

/** The error that fn throws; the test fails when fn throws nothing. */
function caught(fn) {
  try {
    fn();
  } catch (error) {
    return error;
  }
  assert.fail('es wurde nichts geworfen');
}

const schema = {name: [required()]};

test('ein gültiger Body kommt als Objekt zurück', () => {
  assert.deepEqual(parseBody('{"name": "Ada"}', schema), {name: 'Ada'});
});

test('kaputtes JSON ist ein ValidationError mit dem SyntaxError als cause', () => {
  const error = caught(() => parseBody('{name', schema));
  assert.ok(error instanceof ValidationError, `parseBody warf ${error.name}`);
  assert.equal(error.message, 'body: is not valid JSON');
  assert.ok(error.cause instanceof SyntaxError, 'der SyntaxError sollte die Ursache sein');
});

test('null und Arrays sind keine Objekte', () => {
  for (const text of ['null', '[1, 2]']) {
    assert.throws(() => parseBody(text, schema), {name: 'ValidationError', message: 'body: must be a JSON object'}, `bei ${text}`);
  }
});

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

error.errors aus einem beliebigen Fehler lesen

import {parseBody, required} from "./validate.js";

try {
  parseBody("{name", {name: [required()]});
} catch (error) {
  for (const problem of error.errors) console.log(problem.message);
}

Was Node.js ausgibt

TypeError: error.errors is not iterable

Warum, und die Lösung

Nur ein AggregateError hat ein Array errors. Hier warf parseBody für das kaputte JSON einen einzelnen ValidationError, error.errors ist also undefined. Prüfen Sie zuerst den Typ: if (error instanceof AggregateError) listen Sie error.errors auf, else if (error instanceof ValidationError) zeigen Sie error.message, und alles andere werfen Sie weiter.

null validieren

import {validate, required} from "./validate.js";

const data = JSON.parse("null");
validate({name: [required()]}, data);

Was Node.js ausgibt

TypeError: Cannot read properties of null (reading 'name')

Warum, und die Lösung

"null" ist gültiges JSON, ebenso Arrays und Zahlen, ein erfolgreiches JSON.parse heißt also nicht, dass Sie ein Objekt haben. validate liest data[field] und stürzt bei null ab. Prüfen Sie zuerst die Form, wie es parseBody tut: Werfen Sie einen ValidationError, wenn typeof data nicht "object" ist, data null ist oder ein Array.

Eine Regel importieren, die die Bibliothek nicht exportiert

import {validate, required, maxLength} from "./validate.js";

validate({name: [required(), maxLength(5)]}, {name: "Ada"});

Was Node.js ausgibt

SyntaxError: The requested module './validate.js' does not provide an export named 'maxLength'

Warum, und die Lösung

Der Import wird geprüft, bevor irgendein Code läuft, und validate.js hat noch keinen Export namens maxLength. Ergänzen Sie export const maxLength = … in validate.js (die kleine Aufgabe tut es), oder importieren Sie nur Namen, die es gibt.

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

Regeln sind Funktionen

Eine Regel wie minLength(2) gibt eine Prüfung zurück: eine Funktion, die einen Wert bekommt und das Problem als Text zurückgibt, oder null, wenn der Wert in Ordnung ist. Ein Schema listet die Prüfungen pro Feld auf: {name: [required(), minLength(2)]}. validate(schema, data) führt sie der Reihe nach aus und hält beim ersten Problem jedes Feldes an, ein fehlender Name sagt also "is required" und nicht zusätzlich "must have at least 2 characters". Eine neue Regel ändert validate nie.

Alle Probleme auf einmal

Ein Formular mit drei Fehlern soll drei Meldungen bekommen, nicht eine pro Versuch. validate sammelt pro verletztem Feld einen ValidationError, jeweils mit einer Eigenschaft field, und wirft dann einen einzigen AggregateError, dessen Array errors sie alle enthält. Der Aufrufer prüft error instanceof AggregateError und listet error.errors auf. parseBody(text, schema) macht aus kaputtem JSON einen ValidationError für das Feld body, mit dem SyntaxError als cause.

Eine Bibliothek bringt Tests mit

validate.test.js importiert validate.js und prüft es mit node --test. assert.throws nimmt eine Klasse, oder ein Objekt, dessen Eigenschaften passen müssen, etwa {name: "ValidationError", field: "body"}, oder eine Funktion, die den Fehler selbst prüft und true zurückgibt. Vergleichen Sie die Liste der Felder mit deepEqual. Ein Test, der einen Fehler nur fängt und innerhalb von catch prüft, besteht auch dann, wenn nichts geworfen wird, nehmen Sie deshalb lieber assert.throws.

Quellen

Zuletzt geprüft am 5. Oktober 2026