Skip to content

Error-Contract (Framework-Infrastruktur)

Einheitlicher Umgang mit Errors durch alle Layer — Klassen, HTTP-Mapping, Response-Format, Client-Handling, Retry, i18n, Logging. Feature-Autoren werfen typisierte Errors, das Framework macht den Rest.

Kein Feature, sondern Framework-Infra wie Outbox oder Observability.

Status (Stand: April 2026)

v1 ist gebaut. Dokumentiert ist hier der Vollausbau — was im Code wirklich drin ist, steht im Block “v1 — was ist drin” unten. Der Rest wartet auf angrenzende Infrastruktur (Observability, Secrets, Rate-Limiting, Client-SDK). Reihenfolge siehe priorisierung.md.

v1 — was ist drin

BausteinStatus
KumikoError Basis-Klasse (code, httpStatus, i18nKey, i18nParams, details, cause)
7 konkrete Klassen: ValidationError, AccessDeniedError, NotFoundError, ConflictError, VersionConflictError, UnprocessableError, InternalError
Dispatcher-Catch + Auto-Wrap unerwarteter Exceptions als InternalError
Response-Format vereinheitlicht (siehe “Abweichung” unten)
Zod-Issue → ValidationError.details.fields
Validation-Hook-Fehler → ValidationError.details.fields (gleicher Topf wie Zod)
Cause-Chain (new Error(..., { cause })) — server-side im Log, nie im Wire-Body
writeFailure(err) / failNotFound(entity, id) / failUnprocessable(reason, details) als Handler-Convenience
Serializer unterdrueckt details bei InternalError (Leak-Schutz)
Feature-spezifische Reason-Codes via details.reason (siehe “Feature-Reasons” unten)
Goldstandard-Integration-Test error-contract.integration.ts (13 Tests, 1 pro Klasse + Cross-Cutting)

v2 — kommt mit der jeweiligen Welle

Nicht “vergessen” — bewusst verschoben bis die angrenzende Infra da ist:

BausteinBlockiert auf
AuthenticationError (401)Auth-Feature (core-auth.md)
FeatureDisabledError (403)Feature-Toggles
RateLimitError (429) mit Retry-AfterRate-Limiting (core-rate-limiting.md)
UpgradeRequiredError (426)API-Evolution (api-evolution.md)
DuplicateError (409) als eigene Klasseaktuell via ConflictError + details.reason abgedeckt
ServiveUnavailableError (503)braucht Health-Checker
i18n-Default-Translations (de, en)i18n-Infrastruktur-Ausbau
Metrics-Counter kumiko_handler_errors_total{code}Observability (Welle 1)
Trace-Span-Error-MarkerObservability (Welle 1)
Secret-Guard im Error-Serializer (Brand-Walk)Secrets-Feature (core-secrets.md)
Client-SDK Error-Router + Auto-Retry-EngineClient-SDK (Schicht 5)

Abweichungen vom Plan (Ist-Stand-Transparenz)

Der Code weicht in zwei kleinen Punkten von der Idealbeschreibung unten ab — bewusst, mit Begruendung:

  1. Response-Format fuer /write + /batch hat isSuccess: false an der Wurzel, nicht nur { error: {...} }. Begruendung: Symmetrie zum Success-Format { isSuccess: true, data: ... }. Der Client kann an einem Boolean abfragen, statt am HTTP-Status oder an der error-Existenz. Fuer /query + /command bleibt die schlanke Form { error: {...} }, weil Success dort auch schlank ist ({ data } bzw. { ok }). Batch-Antworten tragen zusaetzlich failedIndex + results damit der Client weiss, welche Command gescheitert ist.

  2. NotFoundError generiert automatisch eine details.reason im Stil <snake_entity>_not_found (z.B. "unit_not_found"). Das ist eine stabile Kennung fuer Tests + Client-Logik, zusaetzlich zum generischen code: "not_found". Andere Error-Klassen setzen details.reason nicht automatisch — nur der Handler, wenn er einen Feature-Reason mitgeben will.

Feature-Reasons: wie feature-spezifische Codes ausgedrueckt werden

Die 7 Klassen geben Grob-Kategorien (not_found, access_denied, unprocessable, …). Feature-spezifische Differenzierung (z.B. “invoice_already_paid” vs. “invoice_not_in_draft”) lebt unter details.reason:

throw new UnprocessableError("order.already_cancelled", {
i18nKey: "orders.errors.alreadyCancelled",
details: { orderId: event.orderId },
});
// → Wire:
// {
// code: "unprocessable",
// i18nKey: "orders.errors.alreadyCancelled",
// details: { reason: "order.already_cancelled", orderId: 42 },
// ...
// }

Konvention: <feature>.<short_key> fuer Feature-spezifisches, einfache snake_strings fuer Framework-weit. Kein Typ-Enforcement — aber ein Lint-Guard pruefst die Form (scripts/guard-error-reasons.ts, integriert in yarn kumiko check):

^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$
  • Lowercase ASCII, Underscores fuer Wort-Breaks
  • Optional Dot-Namespaces fuer Feature-Scoped Reasons
  • Keine camelCase, Leerzeichen, Minus, Leading-Digits

Der Guard pruefft String-Literale in:

  • new UnprocessableError(X, ...) / failUnprocessable(X, ...) (erstes Argument)
  • Object-Literale mit reason: "X" (z.B. in details: { reason: "..." })

Referenzen auf Konstanten (FrameworkReasons.staleState, BmcReasons.notYours) werden nicht gepruefft — die Konstanten selbst sind die kanonische Form. Soll ein Reason wiederverwendet werden, immer als Konstante ablegen statt den Literal zu kopieren.

Prinzipien

  1. Feste Klassen-Hierarchie, keine eigenen Error-Klassen in Features. ~12 Klassen decken alles ab.
  2. Kein manuelles try/catch fuer erwartete Errors. throw new NotFoundError(...) reicht.
  3. Unerwartete Exceptions werden gewrappt als InternalError mit Stack im Log, sanitized fuer Client.
  4. Response-Format ist einheitlich — jeder 4xx/5xx hat dasselbe Schema.
  5. i18n ist First-Class — jeder Error traegt i18nKey + i18nParams.
  6. Cause-Chain bleibt im Log, nicht im Response. Forensik vs Sicherheit.
  7. Client-SDK macht Auto-Retry fuer 429/503/Network — Feature-Code muss nichts wissen.
  8. Secret-Brand-Guard auch bei Errors. Wer Secret<> in Error-Details stopft: Fehler.

Klassen-Hierarchie

KumikoError (abstract)
├─ ValidationError (400) Zod-Validation gescheitert, Details pro Feld
├─ AuthenticationError (401) JWT/PAT fehlt oder ungueltig
├─ AccessDeniedError (403) Rolle reicht nicht
├─ FeatureDisabledError (403) Feature fuer Tenant deaktiviert
├─ NotFoundError (404) Entity/Handler nicht da
├─ UpgradeRequiredError (426) Min-Client-Version nicht erfuellt
├─ ConflictError (409) Allgemein
│ ├─ VersionConflictError (409) Optimistic Lock
│ └─ DuplicateError (409) Unique-Constraint verletzt
├─ UnprocessableError (422) Business-Rule verletzt
├─ RateLimitError (429) mit Retry-After
├─ InternalError (500) Auto-Wrap unerwarteter Exceptions
└─ ServiceUnavailableError (503) DB/Redis/externer Service weg

Gemeinsame Basis-Attribute

abstract class KumikoError extends Error {
abstract readonly code: string; // stabiler Client-Identifier
abstract readonly httpStatus: number;
readonly i18nKey: string;
readonly i18nParams?: Record<string, unknown>;
readonly details?: unknown; // strukturierte Zusatzinfos
readonly cause?: Error; // Wrap-Quelle
constructor(opts: { message: string; i18nKey: string; i18nParams?; details?; cause? });
}

Jede konkrete Klasse setzt code und httpStatus fest, bringt eigene Konstruktor-Convenience mit.

Beispiel-Klasse

class NotFoundError extends KumikoError {
readonly code = "not_found";
readonly httpStatus = 404;
constructor(entityName: string, entityId?: string, opts?: Partial<ErrorOpts>) {
super({
message: `${entityName}${entityId ? ` ${entityId}` : ""} not found`,
i18nKey: opts?.i18nKey ?? "errors.notFound",
i18nParams: { entity: entityName, id: entityId, ...opts?.i18nParams },
details: opts?.details,
cause: opts?.cause,
});
}
}

Feature-Autor nutzt:

throw new NotFoundError("order", event.orderId);
// oder mit Custom-i18n:
throw new NotFoundError("order", event.orderId, { i18nKey: "orders.errors.specificNotFound" });

Response-Format

Einheitlich fuer jeden 4xx/5xx-Response:

{
"error": {
"code": "validation_error",
"i18nKey": "errors.validation.failed",
"message": "Validation failed", // dev-facing, Fallback wenn Client-i18n versagt
"details": { /* klassen-spezifisch, strukturiert */ },
"traceId": "abc123...",
"timestamp": "2026-04-15T10:00:00Z"
}
}

details pro Klasse

Klassedetails-Form
ValidationError{ fields: [{ path, code, i18nKey, params? }, ...] }
RateLimitError{ resetAt, remaining, limit, window }
VersionConflictError{ expectedVersion, currentVersion, entityId }
DuplicateError{ field, value }
UpgradeRequiredError{ minVersion, currentVersion }
FeatureDisabledError{ featureName }
UnprocessableErrorFeature-spezifisch (optional)
InternalErrorkein details (keine Internals leaken)

Status-Header

Zusaetzlich zu HTTP-Status:

  • Content-Type: application/json
  • Fuer RateLimitError: Retry-After: <sec>, X-RateLimit-*
  • Fuer UpgradeRequiredError: X-Kumiko-Min-Client-Version: 1.5.0

Kein manuelles try/catch in Handlern

// Feature-Autor schreibt:
r.writeHandler("order.cancel", schema, async (event, ctx) => {
const order = await ctx.db.findById("order", event.orderId);
if (!order) {
throw new NotFoundError("order", event.orderId);
}
if (order.status === "cancelled") {
throw new UnprocessableError("order.already_cancelled", {
i18nKey: "orders.errors.alreadyCancelled",
i18nParams: { orderId: event.orderId },
});
}
// happy path — kein try/catch, kein Error-Handling-Boilerplate
await ctx.db.update("order", event.orderId, { status: "cancelled" });
});

Was das Framework erledigt

  1. Fange alle KumikoError im Dispatcher
  2. Setze HTTP-Status aus .httpStatus
  3. Serialisiere zu Response-Format (mit traceId aus aktuellem Span)
  4. Log-Eintrag mit Full-Context + Stack + Cause-Chain
  5. Markiere Trace-Span als Error mit code-Label
  6. Ggf. Metrics-Counter incrementieren (kumiko_handler_errors_total{code="not_found"})

Wann doch try/catch

Nur in zwei Faellen:

  • Cross-Process-Call wrappen (externe HTTP-API, Redis, etc.) — eigenen Error fangen, in ServiceUnavailableError oder InternalError umwrappen mit cause
  • Business-Entscheidung bei Error (z.B. “wenn NotFound → Default nehmen”) — sehr selten

Nie fuer “Error loggen und Response bauen” — das macht das Framework.

Unerwartete Exceptions auto-wrap

Wenn im Handler etwas Nicht-Kumiko-artiges geworfen wird (TypeError, RangeError, Third-Party-Error, plain Error):

throw new TypeError("cannot read property...")
Framework:
1. Catch im Dispatcher
2. Wrap: new InternalError({ message: "Unexpected error in handler", cause: originalError })
3. Log mit FULL Stack, Context, Cause-Chain
4. Metrics: kumiko_handler_errors_total{code="internal_error"}++
5. Audit-Alarm wenn >threshold
6. Response an Client:
{
"error": {
"code": "internal_error",
"i18nKey": "errors.internal",
"message": "An unexpected error occurred. Please try again or contact support with the traceId.",
"traceId": "abc...",
"timestamp": "..."
}
}

Sanitized: kein Stack, keine Internals im Client. User kriegt Trace-ID fuer Support-Meldung. Operator findet die Ursache ueber Trace-ID im Log.

Validation via Zod

Zod-Fehler werden automatisch in ValidationError gewandelt:

// Dispatcher macht das:
const result = schema.safeParse(event.payload);
if (!result.success) {
throw new ValidationError({
details: {
fields: result.error.issues.map((issue) => ({
path: issue.path.join("."),
code: issue.code, // "too_small", "invalid_type", ...
i18nKey: `errors.validation.${issue.code}`,
params: issue.params, // z.B. { min: 0 }
})),
},
});
}

Feature-Autor schreibt nur das Zod-Schema. Fehler-Output ist automatisch strukturiert + i18n-faehig.

Custom Validation in Hooks

r.hook("validation", "order:entity:order", async (ctx, data) => {
if (data.totalAmount < 0) {
throw new ValidationError({
details: {
fields: [{ path: "totalAmount", code: "must_be_positive", i18nKey: "errors.field.mustBePositive" }],
},
});
}
});

Framework konsolidiert Zod + Hook-Validation — Client sieht einen ValidationError mit allen Fehlern.

i18n-Integration

Default-Translations im Framework

Framework liefert ein komplettes Set an Default-Uebersetzungen fuer alle Fehlerklassen + Validator-Codes:

// packages/framework/src/i18n/errors/de.json (und .en, .fr, etc.)
{
"errors.internal": "Ein unerwarteter Fehler ist aufgetreten. Trace-ID: {traceId}",
"errors.notFound": "{entity} nicht gefunden",
"errors.access.denied": "Keine Berechtigung",
"errors.feature.disabled": "Funktion nicht verfuegbar",
"errors.validation.failed": "Validierung fehlgeschlagen",
"errors.validation.required": "Pflichtfeld",
"errors.validation.too_small": "Wert zu klein (Minimum: {min})",
"errors.validation.too_big": "Wert zu gross (Maximum: {max})",
"errors.validation.invalid_type":"Falscher Typ (erwartet: {expected})",
"errors.rateLimit": "Zu viele Anfragen. Bitte in {seconds}s erneut versuchen.",
"errors.versionConflict": "Die Daten wurden inzwischen geaendert. Bitte neu laden.",
"errors.duplicate": "{field} \"{value}\" existiert bereits",
"errors.upgradeRequired": "Bitte App aktualisieren (min. Version {minVersion})",
"errors.unprocessable": "Aktion nicht moeglich",
"errors.serviceUnavailable": "Service nicht verfuegbar. Bitte spaeter erneut versuchen."
// ...
}

Feature-Override

r.translations({
de: {
"orders.errors.alreadyCancelled": "Auftrag {orderId} wurde bereits storniert",
"orders.errors.alreadyShipped": "Auftrag ist bereits versendet",
},
});
// Dann im Handler:
throw new UnprocessableError("order.already_cancelled", {
i18nKey: "orders.errors.alreadyCancelled",
i18nParams: { orderId: event.orderId },
});

Client uebersetzt via t(error.i18nKey, error.i18nParams).

Fallback

Wenn i18nKey im Client nicht bekannt: error.message zeigen. Verhindert dass User “errors.foo.bar” als Text sieht bei Version-Mismatch.

Client-Side-Handling

Framework liefert Default-Router fuer Errors im Client-SDK:

// Default-Verhalten (ueber alle Apps):
{
"validation_error": → Inline-Form-Errors pro Feld (details.fields)
"authentication": → Logout + Re-Login-Screen
"access_denied": → Toast-Warnung
"feature_disabled": → Toast-Info
"not_found": → Context-spezifisch, Default: Toast
"version_conflict": → Auto-Reload + Toast "Daten wurden geaendert"
"duplicate": → Inline-Error auf dem konfliktären Feld (details.field)
"duplicate_idempotency_key": → Silently swallow (same request already processed)
"stale_state": → Auto-Reload + Toast "Status hat sich geaendert"
"rate_limited": → Auto-Retry nach Retry-After, dann Toast wenn weiter fehlschlaegt
"upgrade_required": → Full-Screen-Update-Prompt (nicht ueberspringbar)
"unprocessable": → Toast mit i18n-Message
"internal_error": → Toast "Unerwarteter Fehler" + "Support kontaktieren mit {traceId}"
"service_unavailable": → Auto-Retry mit Backoff, nach 3 Fehlern Full-Screen "Service offline"
}

Pro Handler-Call kann Feature-Autor das Default-Verhalten ueberschreiben:

const result = await dispatch("order.cancel", payload, {
errorHandling: {
"not_found": (err) => showInlineMessage("Auftrag existiert nicht"),
// Rest: Default
},
});

Optimistic-Locking-Integration

VersionConflictError hat ein spezielles Default-Handling:

  • Toast anzeigen “Daten wurden in der Zwischenzeit geaendert”
  • Automatisch Re-Load der Entity
  • Form-State erhalten wo moeglich
  • Feature-Autor kann benutzerdefinierten Merge-Dialog zeigen

Siehe optimistic-locking.md.

Retry-Semantik

Client-SDK macht automatisch:

Status/CodeAuto-RetryStrategieMax
rate_limited (429)JaRetry-After-Header oder 1s Default3
service_unavailable (503)JaExponential Backoff (1s, 2s, 4s)3
Network-ErrorJaBackoff3
internal_error (500)Optional (konfigurierbar)Ein Versuch nach 1s1
validation_error (400)NeinDirekt UI
access_denied (403)Nein
not_found (404)Nein
conflict / duplicate / version_conflict / stale_state (409)Nein
duplicate_idempotency_key (200 w/ cached body)NeinReuse prior response
unprocessable (422)Nein
upgrade_required (426)Nein

Feature-Code muss nichts wissen. Dispatcher-Client-SDK retryt im Hintergrund, Feature-Handler sieht nur das finale Ergebnis.

Log-Format

Jeder Error produziert einen strukturierten Log-Eintrag:

{
"level": "error",
"msg": "Handler failed: not_found",
"code": "not_found",
"handler": "orders:write:order:cancel",
"httpStatus": 404,
"tenantId": "t-1",
"userId": "u-7",
"sessionId": "s-...",
"traceId": "abc...",
"spanId": "def...",
"requestId": "r-...",
"stack": "Error: order 42 not found\n at ...",
"cause": {
"name": "TypeError",
"message": "...",
"stack": "...",
"cause": null
}
}

Sensitive-Filter

Log-Emitter prueft:

  • Kein Request-Body
  • Keine Auth-Header
  • Keine Secret<>-branded Values
  • Keine Query-Params die wie Tokens aussehen

Siehe observability.md — gleicher Filter wie fuer Spans.

Log-Level

  • error: InternalError, ServiceUnavailableError (echte Probleme)
  • warn: ValidationError, AccessDeniedError, RateLimitError (erwartet, aber protokollieren)
  • info: NotFoundError, ConflictError (erwartet, minimal loggen, nur mit rate-limit)
  • debug: nur im Dev-Mode, detailliert

Feature-Autor kann Level pro Klasse im Config ueberschreiben wenn z.B. ValidationErrors gefluted werden.

Cause-Chain

new MyError(..., { cause: innerErr }) erzeugt eine Kette. Log emittiert die ganze Kette rekursiv. Response an den Client enthaelt nur Top-Level.

Verhindert dass interne Implementation-Details (z.B. “Redis connection refused”) an den Client leaken, waehrend die Forensik im Log voll vorhanden ist.

Error-Serializer-Guard gegen Secrets

Der Error-Serializer, bevor er den Response zusammenbaut:

function serializeError(err: KumikoError, traceId: string): ResponseBody {
// Walk through details, check for Secret<>-branded values
if (containsSecretBrand(err.details)) {
logAlarm("secret_leak_attempt", err);
// Ersetze details durch leeres Objekt, original-Error ins Log
return { error: { ...rest, details: undefined } };
}
return { error: { ...baseFields(err, traceId) } };
}

Wie der Response-Serializer-Guard aus core-secrets.md. Belt-and-suspenders.

Anti-Patterns

throw "string" oder throw new Error(...)

Falsch:

throw "User not allowed"; // ❌ String
throw new Error("Something happened"); // ❌ Untypisiert

Richtig:

throw new AccessDeniedError();
throw new UnprocessableError("order.invalid_state", {
i18nKey: "orders.errors.invalidState",
});

Begruendung: Framework-Catch erkennt Klasse, mapped HTTP-Status, loggt strukturiert. Plain Error wird zum InternalError → 500 statt korrekt 403.

Eigene Error-Klasse in Feature definieren

Falsch:

class OrderCancelError extends Error { ... } // ❌

Richtig:

throw new UnprocessableError("order.cancel_failed", {
i18nKey: "orders.errors.cancelFailed",
details: { reason: "already_shipped" },
});

Begruendung: Framework kennt die 12 Klassen. Eigene Klassen werden zu InternalError gemacht (unknown Error). Nutze UnprocessableError + code/i18nKey fuer Feature-spezifische Differenzierung.

Sensitive-Info in Error-Message

Falsch:

throw new AccessDeniedError({
message: `User ${userEmail} not in role ${role}`, // ❌ PII in Log + ggf. Response
});

Richtig:

throw new AccessDeniedError({
message: "role check failed", // neutral im Log
details: { requiredRole: "admin" }, // ok
// userId wird vom Framework automatisch aus ctx geloggt
});

Begruendung: Error-Messages landen in Logs, Spans und ggf. Error-Reporting-Services (Sentry). PII drin = PII verteilt.

try/catch um jeden Handler

Falsch:

r.writeHandler("x", schema, async (event, ctx) => {
try {
// do stuff
} catch (err) { // ❌ unnoetig
logger.error(err);
throw new InternalError({ cause: err });
}
});

Richtig:

r.writeHandler("x", schema, async (event, ctx) => {
// do stuff — Framework wrapt nicht-Kumiko-Errors automatisch
});

Framework-Ausbau

AusbauWarum
KumikoError-Basis + 12 konkrete KlassenTypisierung
Dispatcher-Catch + HTTP-Status-MappingAuto-Response
Auto-Wrap unerwarteter Exceptions als InternalErrorProd-Safety
Zod-Issue → ValidationError.details.fields-WrapperZod-Integration
Client-SDK Error-Router + Default-Handling pro CodeGleiches Verhalten ueberall
Auto-Retry-Engine fuer 429/503/Network im Client-SDKFeature-Code ohne Retry-Boilerplate
Default-Translations fuer alle Klassen (de, en minimum)i18n out-of-the-box
Log-Emitter mit Cause-Chain + Sensitive-FilterForensik ohne Leaks
Secret<>-Guard im Error-SerializerBelt-and-suspenders
Metrics-Counter kumiko_handler_errors_total{code=...}Beobachtbarkeit
Trace-Span-Error-Marker mit code-LabelObservability-Korrelation

Tests — der Beweis

Unit

  • Jede Error-Klasse: korrekte code, httpStatus, i18nKey
  • Zod-Issue → ValidationError-Details korrekt gemapped
  • Cause-Chain: new A(..., { cause: new B(...) }) → Log hat A + B, Response nur A
  • Sensitive-Filter: Secret-branded in details → Error-Serializer filtert, Log-Alarm

Integration (full-stack)

  • Handler wirft NotFoundError → 404 mit korrektem Response-Format
  • Handler wirft TypeError (unerwartet) → 500 mit internal_error-Code + sanitized Body, Log hat Stack
  • Zod-Schema-Fehler → 400 mit details.fields array, pro Feld path, code, i18nKey
  • ValidationError aus Hook + Zod-Fehler → konsolidiert, beide im details.fields
  • Client-SDK: 429 Response → Auto-Retry nach Retry-After, beim zweiten Versuch ok → Handler-Ergebnis
  • Client-SDK: 503 Response → Exponential-Backoff-Retry 3x, bei letztem Fail → Full-Screen
  • VersionConflictError → Client lädt automatisch neu + Toast
  • UpgradeRequiredError → Client zeigt Update-Screen
  • Error-Response-Format identisch fuer alle Klassen (Schema-Check)
  • Error-Metrics-Counter steigt pro Code
  • Trace-Span hat error=true und code-Label

Sample

Sample mit bewusst provozierten Errors in unterschiedlichen Klassen — zeigt Client-UI-Reaktion und Log-Eintraege.

Build-Reihenfolge

  1. KumikoError-Abstract-Klasse + 12 konkrete Klassen
  2. Dispatcher-Catch + HTTP-Status-Mapping
  3. Response-Format-Serialisierung
  4. Auto-Wrap unerwarteter Exceptions → InternalError
  5. Zod-Issue-Integration in ValidationError
  6. Log-Emitter mit Cause-Chain + Sensitive-Filter
  7. Default-i18n-Set fuer alle Klassen (de + en)
  8. Metrics-Counter + Trace-Span-Error-Marker
  9. Secret<>-Guard im Error-Serializer
  10. Client-SDK Error-Router
  11. Client-SDK Auto-Retry-Engine
  12. Tests (Unit + Integration cross-scenario)
  13. Sample das alle Error-Klassen demonstriert
  14. Migration: bestehende throw new Error(...) im Framework-Code durch typisierte Kumiko-Errors ersetzen