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
| Baustein | Status |
|---|---|
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:
| Baustein | Blockiert auf |
|---|---|
AuthenticationError (401) | Auth-Feature (core-auth.md) |
FeatureDisabledError (403) | Feature-Toggles |
RateLimitError (429) mit Retry-After | Rate-Limiting (core-rate-limiting.md) |
UpgradeRequiredError (426) | API-Evolution (api-evolution.md) |
DuplicateError (409) als eigene Klasse | aktuell 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-Marker | Observability (Welle 1) |
| Secret-Guard im Error-Serializer (Brand-Walk) | Secrets-Feature (core-secrets.md) |
| Client-SDK Error-Router + Auto-Retry-Engine | Client-SDK (Schicht 5) |
Abweichungen vom Plan (Ist-Stand-Transparenz)
Der Code weicht in zwei kleinen Punkten von der Idealbeschreibung unten ab — bewusst, mit Begruendung:
-
Response-Format fuer
/write+/batchhatisSuccess: falsean 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 dererror-Existenz. Fuer/query+/commandbleibt die schlanke Form{ error: {...} }, weil Success dort auch schlank ist ({ data }bzw.{ ok }). Batch-Antworten tragen zusaetzlichfailedIndex+resultsdamit der Client weiss, welche Command gescheitert ist. -
NotFoundErrorgeneriert automatisch einedetails.reasonim Stil<snake_entity>_not_found(z.B."unit_not_found"). Das ist eine stabile Kennung fuer Tests + Client-Logik, zusaetzlich zum generischencode: "not_found". Andere Error-Klassen setzendetails.reasonnicht 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. indetails: { 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
- Feste Klassen-Hierarchie, keine eigenen Error-Klassen in Features. ~12 Klassen decken alles ab.
- Kein manuelles
try/catchfuer erwartete Errors.throw new NotFoundError(...)reicht. - Unerwartete Exceptions werden gewrappt als
InternalErrormit Stack im Log, sanitized fuer Client. - Response-Format ist einheitlich — jeder 4xx/5xx hat dasselbe Schema.
- i18n ist First-Class — jeder Error traegt
i18nKey+i18nParams. - Cause-Chain bleibt im Log, nicht im Response. Forensik vs Sicherheit.
- Client-SDK macht Auto-Retry fuer 429/503/Network — Feature-Code muss nichts wissen.
- 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 wegGemeinsame 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
| Klasse | details-Form |
|---|---|
ValidationError | { fields: [{ path, code, i18nKey, params? }, ...] } |
RateLimitError | { resetAt, remaining, limit, window } |
VersionConflictError | { expectedVersion, currentVersion, entityId } |
DuplicateError | { field, value } |
UpgradeRequiredError | { minVersion, currentVersion } |
FeatureDisabledError | { featureName } |
UnprocessableError | Feature-spezifisch (optional) |
InternalError | kein 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
- Fange alle
KumikoErrorim Dispatcher - Setze HTTP-Status aus
.httpStatus - Serialisiere zu Response-Format (mit traceId aus aktuellem Span)
- Log-Eintrag mit Full-Context + Stack + Cause-Chain
- Markiere Trace-Span als Error mit
code-Label - 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
ServiceUnavailableErroroderInternalErrorumwrappen mitcause - 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/Code | Auto-Retry | Strategie | Max |
|---|---|---|---|
rate_limited (429) | Ja | Retry-After-Header oder 1s Default | 3 |
service_unavailable (503) | Ja | Exponential Backoff (1s, 2s, 4s) | 3 |
| Network-Error | Ja | Backoff | 3 |
internal_error (500) | Optional (konfigurierbar) | Ein Versuch nach 1s | 1 |
validation_error (400) | Nein | Direkt UI | — |
access_denied (403) | Nein | — | — |
not_found (404) | Nein | — | — |
conflict / duplicate / version_conflict / stale_state (409) | Nein | — | — |
duplicate_idempotency_key (200 w/ cached body) | Nein | Reuse 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"; // ❌ Stringthrow new Error("Something happened"); // ❌ UntypisiertRichtig:
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
| Ausbau | Warum |
|---|---|
KumikoError-Basis + 12 konkrete Klassen | Typisierung |
| Dispatcher-Catch + HTTP-Status-Mapping | Auto-Response |
| Auto-Wrap unerwarteter Exceptions als InternalError | Prod-Safety |
Zod-Issue → ValidationError.details.fields-Wrapper | Zod-Integration |
| Client-SDK Error-Router + Default-Handling pro Code | Gleiches Verhalten ueberall |
| Auto-Retry-Engine fuer 429/503/Network im Client-SDK | Feature-Code ohne Retry-Boilerplate |
| Default-Translations fuer alle Klassen (de, en minimum) | i18n out-of-the-box |
| Log-Emitter mit Cause-Chain + Sensitive-Filter | Forensik ohne Leaks |
Secret<>-Guard im Error-Serializer | Belt-and-suspenders |
Metrics-Counter kumiko_handler_errors_total{code=...} | Beobachtbarkeit |
Trace-Span-Error-Marker mit code-Label | Observability-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 mitinternal_error-Code + sanitized Body, Log hat Stack - Zod-Schema-Fehler → 400 mit
details.fieldsarray, pro Feldpath,code,i18nKey ValidationErroraus Hook + Zod-Fehler → konsolidiert, beide imdetails.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 + ToastUpgradeRequiredError→ Client zeigt Update-Screen- Error-Response-Format identisch fuer alle Klassen (Schema-Check)
- Error-Metrics-Counter steigt pro Code
- Trace-Span hat
error=trueundcode-Label
Sample
Sample mit bewusst provozierten Errors in unterschiedlichen Klassen — zeigt Client-UI-Reaktion und Log-Eintraege.
Build-Reihenfolge
KumikoError-Abstract-Klasse + 12 konkrete Klassen- Dispatcher-Catch + HTTP-Status-Mapping
- Response-Format-Serialisierung
- Auto-Wrap unerwarteter Exceptions → InternalError
- Zod-Issue-Integration in
ValidationError - Log-Emitter mit Cause-Chain + Sensitive-Filter
- Default-i18n-Set fuer alle Klassen (de + en)
- Metrics-Counter + Trace-Span-Error-Marker
Secret<>-Guard im Error-Serializer- Client-SDK Error-Router
- Client-SDK Auto-Retry-Engine
- Tests (Unit + Integration cross-scenario)
- Sample das alle Error-Klassen demonstriert
- Migration: bestehende
throw new Error(...)im Framework-Code durch typisierte Kumiko-Errors ersetzen