TypeScript & Funktional

Effect-TS mit Claude Code:
Funktionale TypeScript-Programmierung 2026

6. Mai 2026 12 min Lesezeit von SpockyMagicAI
Effect-TS TypeScript Funktional Claude Code Error-Handling Dependency Injection Schema Fiber
TypeScript ist mächtig — aber Standard-TypeScript reicht für robuste Produktionssysteme oft nicht aus. Fehlende Fehlertypen in Signaturen, unkontrollierte Seiteneffekte, komplizierte Dependency Injection: Das alles löst Effect-TS radikal. In diesem Artikel zeigen wir, wie Claude Code als KI-Pair-Programmer dir hilft, Effect-TS schnell zu verstehen und produktiv einzusetzen — von Effect-Grundlagen bis zu echten Concurrent-Patterns.

Inhalt

  1. Effect Grundlagen: Effect-Typ, succeed, fail, pipe
  2. Error-Handling mit catchAll, catchTag und typed Errors
  3. Dependency Injection: Context.Tag, Layer, Effect.provide
  4. Schema-Validierung: Struct, decode, custom Schemas
  5. Fiber & Concurrency: fork, all, race, interrupt
  6. Praktische Patterns: HTTP, Database, Config, Testing

EinleitungWarum Effect-TS im Jahr 2026?

Effect-TS hat sich in den letzten zwei Jahren von einem Nischenprojekt zur ernsthaften Alternative für TypeScript-Backends entwickelt. Wer einmal echte typsichere Fehlerbehandlung kennt — also Fehler im Typsystem statt im Laufzeit-Exception-Stack — will nicht mehr zurück.

🔒

Typsichere Fehler

Fehlertypen erscheinen direkt in der Funktionssignatur. Kein überraschendes throw mehr.

🧩

Dependency Injection

Services als Typen — keine globalen Singletons, kein IoC-Container von Hand.

Strukturierte Concurrency

Fiber-basierte Parallelität mit sauberem Lifecycle — keine ungekillten Promises.

Schema-Validierung

Eingebaute Validierung, Parsing und Serialisierung — kein Zod extra nötig.

Claude Code + Effect-TS: Claude Code kennt die Effect-API sehr gut. Mit dem richtigen Prompt kannst du komplette Layer-Architekturen, Schema-Definitionen und Fiber-basierte Workflows generieren lassen — und musst die Typen selbst nur noch reviewen statt schreiben.

Lass uns mit dem Setup beginnen. Installation mit npm oder pnpm:

Terminal
npm install effect # oder pnpm add effect

Das war's — Effect-TS hat keine Peer-Dependencies. Alles ist in einem Package: Effect-Typ, Schema, Layer, Fiber, Concurrency-Utilities.

01 — GrundlagenEffect-Typ, succeed, fail, pipe und runPromise

Der zentrale Typ in Effect-TS ist Effect<A, E, R>. Diese drei Typparameter bedeuten:

A Success-Typ

Was zurückgegeben wird, wenn alles klappt.

E Error-Typ

Welche Fehler können auftreten? Explizit im Typsystem — kein unknown.

R Requirements-Typ

Welche Services/Dependencies braucht dieser Effect? Werden via Layer aufgelöst.

Einfache Effects erstellen

grundlagen.ts
import { Effect } from "effect" // Effect.succeed: Ein Wert ohne Fehler, ohne Requirements const hallo: Effect.Effect<string, never, never> = Effect.succeed("Hallo Effect-TS!") // Effect.fail: Ein typisierter Fehler const fehler = Effect.fail(new Error("Etwas ist schiefgelaufen")) // Effect.sync: Synchrone Berechnung mit möglichem throw const zufallszahl = Effect.sync(() => Math.random()) // Effect.tryPromise: Async-Funktion die ein Promise zurückgibt const fetchBenutzer = Effect.tryPromise({ try: () => fetch("https://api.example.com/benutzer/1").then(r => r.json()), catch: (err) => new FetchFehler({ cause: err }) })

pipe: Effects kombinieren

Effects werden nicht direkt ausgeführt — sie sind Beschreibungen von Berechnungen. Mit pipe und Effect.map/Effect.flatMap werden sie kombiniert:

pipe-beispiel.ts
import { Effect, pipe } from "effect" const begruessung = pipe( Effect.succeed("Welt"), Effect.map((name) => `Hallo, ${name}!`), Effect.map((gruss) => gruss.toUpperCase()) ) // Alternativ: Effect.gen für async/await-Style const begruessungGen = Effect.gen(function* () { const name = yield* Effect.succeed("Welt") const gruss = `Hallo, ${name}!` return gruss.toUpperCase() }) // Effect ausführen: runPromise wandelt Effect in ein Promise um Effect.runPromise(begruessung).then(console.log) // Output: "HALLO, WELT!"

Warum nicht einfach async/await?

Kriterium async/await Effect-TS
Fehlertypen in Signatur ❌ Nur Promise<T> Effect<T, FehlerTyp, R>
Dependency Injection ❌ Globale Imports / Singletons ✅ Layer + Context.Tag
Concurrency-Kontrolle ❌ Promise.all — keine Cancellation ✅ Fiber mit interrupt + race
Testbarkeit ❌ Mocking von Hand ✅ Layer-Swap in Tests
Lernkurve ✅ Bekannt, flach ⚠️ Steil, aber lohnend

Claude Code Tipp: Frag Claude Code nach "Effect.gen statt pipe" wenn du die async/await-Syntax bevorzugst. Claude kennt beide Stile und kann die idiomatischere Variante für deinen Use Case vorschlagen.

02 — Error-HandlingcatchAll, catchTag und typisierte Fehler

Das stärkste Feature von Effect-TS ist sein Fehlermodell. Fehler werden nicht nur geworfen und irgendwo gefangen — sie sind Teil des Typsystems. Welche Fehler eine Funktion werfen kann, siehst du sofort in IntelliSense.

Tagged Errors: Fehler mit Identität

errors.ts
import { Data, Effect } from "effect" // Tagged Errors: Mit _tag für Discriminated Unions class NetzwerkFehler extends Data.TaggedError("NetzwerkFehler")<{ statusCode: number url: string }> {} class ParseFehler extends Data.TaggedError("ParseFehler")<{ eingabe: string erwartet: string }> {} class AuthFehler extends Data.TaggedError("AuthFehler")<{ benutzer: string }> {} // Funktion mit mehreren möglichen Fehlern — alle im Typ sichtbar const ladeBenutzer = (id: string): Effect.Effect< Benutzer, NetzwerkFehler | ParseFehler | AuthFehler > => Effect.gen(function* () { const antwort = yield* Effect.tryPromise({ try: () => fetch(`/api/benutzer/${id}`), catch: () => new NetzwerkFehler({ statusCode: 0, url: `/api/benutzer/${id}` }) }) if (!antwort.ok) { return yield* Effect.fail( new NetzwerkFehler({ statusCode: antwort.status, url: antwort.url }) ) } const daten = yield* Effect.tryPromise({ try: () => antwort.json(), catch: () => new ParseFehler({ eingabe: "response body", erwartet: "JSON" }) }) return daten as Benutzer })

catchTag: Einzelne Fehlertypen behandeln

error-handling.ts
import { Effect } from "effect" const programm = pipe( ladeBenutzer("123"), // Nur NetzwerkFehler behandeln — ParseFehler und AuthFehler bleiben Effect.catchTag("NetzwerkFehler", (fehler) => Effect.gen(function* () { console.error(`HTTP ${fehler.statusCode} bei ${fehler.url}`) return standardBenutzer }) ), // ParseFehler als nicht-kritisch loggen Effect.catchTag("ParseFehler", (fehler) => { console.warn(`Parse-Fehler: erwartet ${fehler.erwartet}`) return Effect.succeed(gasterBenutzer) }), // Alle restlichen Fehler (AuthFehler) auf einmal fangen Effect.catchAll((fehler) => Effect.fail(new Error(`Unbehandelter Fehler: ${fehler._tag}`)) ) )

Effect.either: Fehler als Wert behandeln

Manchmal möchtest du Fehler nicht fangen, sondern als regulären Wert weiterverarbeiten. Effect.either macht das möglich:

either.ts
import { Effect, Either } from "effect" const ergebnis = yield* Effect.either(ladeBenutzer("123")) // ergebnis: Either<Benutzer, NetzwerkFehler | ParseFehler | AuthFehler> if (Either.isRight(ergebnis)) { console.log("Benutzer:", ergebnis.right) } else { console.log("Fehler:", ergebnis.left._tag) } // orElse: Fallback-Effect bei Fehler const mitFallback = Effect.orElse( ladeBenutzer("123"), () => ladeBenutzer("fallback") )

Pattern: Fehler-Hierarchie aufbauen

Definiere Basis-Fehlerklassen für deine Domain (z.B. AppFehler) und leite spezifischere Fehler davon ab. So kannst du mit catchTag granular reagieren oder mit catchAll alle AppFehler auf einmal behandeln.

Claude Code kann dir diese Hierarchie automatisch generieren: "Erstelle eine Effect-TS Fehler-Hierarchie für eine E-Commerce-App mit Checkout-, Inventar- und Zahlungsfehlern."

03 — Dependency InjectionContext.Tag, Layer und Effect.provide

Dependency Injection in TypeScript war bisher entweder zu einfach (globale Singletons) oder zu komplex (IoC-Container wie InversifyJS). Effect-TS löst das elegant mit Context.Tag und Layer.

Services definieren: Context.Tag

services.ts
import { Context, Effect, Layer } from "effect" // Service-Interface definieren interface DatenbankService { readonly findeBenutzer: (id: string) => Effect.Effect<Benutzer, DbFehler> readonly speichereBenutzer: (b: Benutzer) => Effect.Effect<void, DbFehler> } // Tag erstellen: Identität des Service im Dependency-System const DatenbankService = Context.GenericTag<DatenbankService>("DatenbankService") // Email-Service interface EmailService { readonly sendeWillkommen: (email: string) => Effect.Effect<void, EmailFehler> } const EmailService = Context.GenericTag<EmailService>("EmailService") // Logger-Service interface LoggerService { readonly info: (msg: string) => Effect.Effect<void> readonly error: (msg: string) => Effect.Effect<void> } const LoggerService = Context.GenericTag<LoggerService>("LoggerService")

Services verwenden: yield* Tag

benutzer-service.ts
// Business-Logik: Dependencies sind im R-Typ sichtbar const registriereBenutzer = ( email: string, name: string ): Effect.Effect< Benutzer, DbFehler | EmailFehler, DatenbankService | EmailService | LoggerService // R: Benötigte Services > => Effect.gen(function* () { const db = yield* DatenbankService const email_svc = yield* EmailService const logger = yield* LoggerService yield* logger.info(`Registriere Benutzer: ${email}`) const benutzer: Benutzer = { id: crypto.randomUUID(), email, name } yield* db.speichereBenutzer(benutzer) yield* email_svc.sendeWillkommen(email) yield* logger.info(`Benutzer ${benutzer.id} erfolgreich registriert`) return benutzer })

Layer: Implementierungen bereitstellen

layers.ts
import { Layer, Effect, Console } from "effect" import postgres from "postgres" // Echter PostgreSQL-Layer const PostgresDatenbankLayer = Layer.effect( DatenbankService, Effect.gen(function* () { const sql = postgres(process.env.DATABASE_URL!) return DatenbankService.of({ findeBenutzer: (id) => Effect.tryPromise({ try: () => sql`SELECT * FROM benutzer WHERE id = ${id}`.then(r => r[0]), catch: (e) => new DbFehler({ cause: e }) }), speichereBenutzer: (b) => Effect.tryPromise({ try: () => sql`INSERT INTO benutzer ${sql(b)}`, catch: (e) => new DbFehler({ cause: e }) }) }) }) ) // In-Memory-Layer für Tests const InMemoryDatenbankLayer = Layer.succeed( DatenbankService, DatenbankService.of({ findeBenutzer: (id) => Effect.succeed({ id, email: "test@test.de", name: "Test" }), speichereBenutzer: () => Effect.succeed(undefined) }) ) // Layer zusammenbauen und bereitstellen const AppLayer = Layer.mergeAll( PostgresDatenbankLayer, SmtpEmailLayer, ConsoleLoggerLayer ) // Effect mit Layer ausführen Effect.runPromise( registriereBenutzer("anna@example.de", "Anna Müller").pipe( Effect.provide(AppLayer) ) )

Layer Layer-Komposition

Layer.mergeAll kombiniert unabhängige Layer. Layer.provide löst Abhängigkeiten zwischen Layern auf — wenn EmailLayer den LoggerLayer braucht, reicht EmailLayer.pipe(Layer.provide(LoggerLayer)).

Das Typsystem prüft, ob alle Dependencies aufgelöst sind. Vergisst du einen Layer, kompiliert der Code nicht.

04 — Schema-ValidierungStruct, decode, custom Schemas

Effect-TS kommt mit einem eingebauten Schema-System. Keine externe Bibliothek nötig — Schema ist Teil von Effect und integriert sich nahtlos mit dem Effect-Typ.

Basis-Schemas definieren

schemas.ts
import { Schema } from "effect" // Einfache Typen const BenutzerSchema = Schema.Struct({ id: Schema.String.pipe(Schema.minLength(1)), email: Schema.String.pipe(Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), name: Schema.String.pipe(Schema.minLength(2), Schema.maxLength(100)), alter: Schema.Number.pipe(Schema.int(), Schema.between(0, 150)), rolle: Schema.Literal("admin", "benutzer", "gaest"), erstelltAm: Schema.DateFromString // Parsed String → Date }) // TypeScript-Typ aus Schema ableiten type Benutzer = Schema.Schema.Type<typeof BenutzerSchema> // Nested Schemas const AdresseSchema = Schema.Struct({ strasse: Schema.String, hausnummer: Schema.String, plz: Schema.String.pipe(Schema.pattern(/^\d{5}$/)), stadt: Schema.String }) const BenutzerMitAdresseSchema = Schema.Struct({ ...BenutzerSchema.fields, adresse: Schema.optional(AdresseSchema) })

Validierung und Dekodierung

validation.ts
import { Schema, Effect } from "effect" // Schema.decode: Gibt Effect zurück (integriert in Effect-Pipeline) const validiereBenutzer = (eingabe: unknown) => Schema.decode(BenutzerSchema)(eingabe) // Rückgabe: Effect<Benutzer, ParseError> // In einer Effect-Pipeline verwenden const verarbeiteApiAntwort = Effect.gen(function* () { const rohdaten = yield* fetchBenutzerDaten() const benutzer = yield* Schema.decode(BenutzerSchema)(rohdaten) return benutzer // Typ: Benutzer — vollständig validiert }) // Schema.parse: Synchron, wirft bei Fehler const parseSync = Schema.decodeUnknownSync(BenutzerSchema) // Encoding: Objekt → externe Darstellung (z.B. für API-Antworten) const BenutzerApiSchema = BenutzerSchema.pipe( Schema.transform( Schema.Struct({ id: Schema.String, displayName: Schema.String }), { decode: (b) => ({ id: b.id, displayName: b.name }), encode: (a) => ({ ...standardBenutzer, id: a.id, name: a.displayName }) } ) )

Custom Schemas und Brand-Types

custom-schema.ts
import { Schema, Brand } from "effect" // Brand: Nominales Typsystem — UserId ≠ string type UserId = string & Brand.Brand<"UserId"> const UserIdSchema = Schema.String.pipe( Schema.minLength(36), Schema.maxLength(36), Schema.pattern(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i), Schema.brand("UserId") ) // Schema.decode(UserIdSchema) gibt Effect<UserId, ParseError> zurück // Euro-Betrag mit Validierung type EuroBetrag = number & Brand.Brand<"EuroBetrag"> const EuroBetragSchema = Schema.Number.pipe( Schema.positive(), Schema.multipleOf(0.01), // Max 2 Dezimalstellen Schema.brand("EuroBetrag") ) // Array-Schemas const BenutzerListeSchema = Schema.Array(BenutzerSchema) const ApiAntwortSchema = Schema.Struct({ daten: Schema.Array(BenutzerSchema), gesamt: Schema.Number, seite: Schema.Number, hatNaechsteSeite: Schema.Boolean })

Schema Schema vs. Zod

Effect Schema ist Zod sehr ähnlich — aber direkt im Effect-Ökosystem. Kein Wrapping in Effect.tryPromise nötig. ParseError ist ein vollständig typisierter Fehlertyp. Und Schema.transform ermöglicht bidirektionale Konvertierungen.

05 — Fiber & Concurrencyfork, all, race und interrupt

Concurrency in JavaScript ist durch Promises einfach — aber schwer zu kontrollieren. Wie cancelt man ein Promise? Wie wartet man auf mehrere Promises und unterbricht den Rest wenn eines fertig ist? Effect-TS löst das mit Fibers: leichtgewichtigen, unterbrechbaren Green Threads.

Fiber.fork: Parallele Ausführung

fibers.ts
import { Effect, Fiber } from "effect" // Effect.fork: Effect im Hintergrund starten, gibt Fiber zurück const paralleleAufgaben = Effect.gen(function* () { // Beide starten sofort, ohne auf das Ergebnis zu warten const faser1 = yield* Effect.fork(ladeBenutzerDaten()) const faser2 = yield* Effect.fork(ladeProduktDaten()) // Hier kann weiteres Werk erledigt werden... yield* bereiteAntwortStrukturVor() // Jetzt auf die Ergebnisse warten const benutzer = yield* Fiber.join(faser1) const produkte = yield* Fiber.join(faser2) return { benutzer, produkte } }) // Fiber.interrupt: Sauber abbrechen const mitTimeout = Effect.gen(function* () { const faser = yield* Effect.fork(langsameBerechnunng()) yield* Effect.sleep("5 seconds") // Nach 5 Sekunden: Fiber sauber stoppen const ergebnis = yield* Fiber.interrupt(faser) console.log("Fiber unterbrochen:", ergebnis._tag) })

Effect.all: Parallele Effects

concurrency.ts
import { Effect } from "effect" // Effect.all: Alle parallel ausführen const alleDaten = Effect.all( { benutzer: ladeBenutzer("123"), bestellungen: ladeBestellungen("123"), empfehlungen: ladeEmpfehlungen("123") }, { concurrency: "unbounded" } // alle gleichzeitig ) // Mit Concurrency-Limit: max 3 gleichzeitig const batchVerarbeitung = (ids: string[]) => Effect.all( ids.map((id) => verarbeiteEintrag(id)), { concurrency: 3 } ) // Effect.race: Erster gewinnt, Rest wird unterbrochen const schnellsteSuche = Effect.race( sucheInElasticsearch("anfrage"), sucheInPostgres("anfrage") ) // Effect.timeout: Automatischer Abbruch const mitLimit = ladeExterneDaten().pipe( Effect.timeout("3 seconds"), Effect.catchTag("TimeoutException", () => Effect.succeed(cachedFallbackDaten) ) )

Strukturierte Concurrency in der Praxis

structured-concurrency.ts
import { Effect, Queue, Fiber } from "effect" // Worker-Pool: N parallele Verarbeiter const erstelleWorkerPool = <A, E>( aufgaben: A[], verarbeite: (a: A) => Effect.Effect<void, E>, parallelitaet = 5 ) => Effect.gen(function* () { const warteschlange = yield* Queue.bounded<A>(aufgaben.length) yield* Queue.offerAll(warteschlange, aufgaben) const worker = Effect.gen(function* () { while (true) { const aufgabe = yield* Queue.poll(warteschlange) if (!aufgabe) break yield* verarbeite(aufgabe._tag === "Some" ? aufgabe.value : aufgabe) } }) yield* Effect.all( Array.from({ length: parallelitaet }, () => worker), { concurrency: parallelitaet } ) })

Fiber Fiber vs. Promise

Promises können nicht gecancelt werden. Fibers schon. Wenn ein Fiber-Elternteil stirbt, werden alle Kind-Fibers automatisch unterbrochen. Das verhindert Memory Leaks und Zombie-Tasks in langlaufenden Servern.

06 — Praktische PatternsHTTP, Database, Config, Testing

Jetzt kombinieren wir alles zu realen Anwendungsfällen. Diese Patterns sind direkt in Produktionscode einsetzbar.

HTTP-Client mit Effect

http-client.ts
import { HttpClient, HttpClientRequest } from "@effect/platform" import { Effect, Schema } from "effect" // Effect Platform: HTTP-Client mit Effect-Integration const ladeGithubRepo = (besitzer: string, repo: string) => Effect.gen(function* () { const client = yield* HttpClient.HttpClient const antwort = yield* client.get( `https://api.github.com/repos/${besitzer}/${repo}` ).pipe( HttpClient.filterStatusOk, // Fehler bei !2xx Effect.flatMap(r => r.json) ) return yield* Schema.decode(GithubRepoSchema)(antwort) }) // Mit Retry-Policy const resilenterRequest = ladeGithubRepo("facebook", "react").pipe( Effect.retry({ times: 3, schedule: Schedule.exponential("1 second") }), Effect.timeout("10 seconds"), Effect.provide(HttpClient.layer) )

Database-Layer mit Postgres

db-layer.ts
import { Effect, Layer, Context } from "effect" // Repository-Pattern mit Effect interface BenutzerRepository { readonly finde: (id: UserId) => Effect.Effect<Option.Option<Benutzer>, DbFehler> readonly findeAlle: (limit?: number) => Effect.Effect<Benutzer[], DbFehler> readonly erstelle: (daten: BenutzerErstellen) => Effect.Effect<Benutzer, DbFehler> readonly loesche: (id: UserId) => Effect.Effect<boolean, DbFehler> } const BenutzerRepository = Context.GenericTag<BenutzerRepository>("BenutzerRepository") // Postgres-Implementierung const PostgresBenutzerRepository = Layer.effect( BenutzerRepository, Effect.gen(function* () { const db = yield* DatenbankService return BenutzerRepository.of({ finde: (id) => db.query(`SELECT * FROM benutzer WHERE id = $1`, [id]).pipe( Effect.map(r => r.rows[0] ? Option.some(r.rows[0]) : Option.none()) ), findeAlle: (limit = 100) => db.query(`SELECT * FROM benutzer LIMIT $1`, [limit]).pipe( Effect.map(r => r.rows) ), erstelle: (daten) => db.query( `INSERT INTO benutzer (id, email, name) VALUES ($1, $2, $3) RETURNING *`, [crypto.randomUUID(), daten.email, daten.name] ).pipe(Effect.map(r => r.rows[0])), loesche: (id) => db.query(`DELETE FROM benutzer WHERE id = $1`, [id]).pipe( Effect.map(r => r.rowCount > 0) ) }) }) )

Config-Layer: Konfiguration als Service

config-layer.ts
import { Config, Effect, Layer } from "effect" // Effect Config: Typsichere Umgebungsvariablen const AppConfig = Config.all({ datenbankUrl: Config.string("DATABASE_URL"), jwtSecret: Config.string("JWT_SECRET"), port: Config.number("PORT").pipe(Config.withDefault(3000)), debug: Config.boolean("DEBUG").pipe(Config.withDefault(false)), erlaubteDomains: Config.array(Config.string(), "ERLAUBTE_DOMAINS") }) // Config in einem Layer verfügbar machen const KonfigurationsLayer = Layer.effect( KonfigurationsService, Effect.gen(function* () { const config = yield* AppConfig return KonfigurationsService.of({ datenbankUrl: config.datenbankUrl, jwtSecret: config.jwtSecret, port: config.port, debug: config.debug }) }) ) // Verwendung: Config-Werte direkt in Effects const startServer = Effect.gen(function* () { const config = yield* AppConfig console.log(`Server startet auf Port ${config.port}`) })

Testing mit Layer-Swap

tests.spec.ts
import { Effect, Layer } from "effect" import { describe, it, expect } from "vitest" // Test-Layer: Alle Services gemockt const TestLayer = Layer.mergeAll( InMemoryDatenbankLayer, // Echter DB-Code, In-Memory-Daten MockEmailLayer, // Emails werden nur geloggt ConsoleLoggerLayer // Echter Logger ) describe("BenutzerRegistrierung", () => { it("registriert Benutzer erfolgreich", async () => { const ergebnis = await Effect.runPromise( registriereBenutzer("test@test.de", "Test User").pipe( Effect.provide(TestLayer) // Swap: Prod → Test Layer ) ) expect(ergebnis.email).toBe("test@test.de") expect(ergebnis.id).toBeDefined() }) it("schlägt bei ungültiger Email fehl", async () => { const ergebnis = await Effect.runPromise( registriereBenutzer("keine-email", "Test").pipe( Effect.provide(TestLayer), Effect.flip // Fehler wird zum Erfolg, Erfolg zum Fehler ) ) expect(ergebnis._tag).toBe("ValidationFehler") }) })

Fazit: Das Layer-System macht Testing trivial. Kein jest.mock, kein vi.spyOn auf globale Imports — einfach den Test-Layer statt des Prod-Layers providieren. Der Code wird dadurch automatisch testbar, weil Dependencies nie hartcodiert sind.

Fehler-Hierarchie für größere Systeme

error-hierarchy.ts
import { Data } from "effect" // Domain-spezifische Fehler-Hierarchie class AppFehler extends Data.TaggedError("AppFehler")<{ message: string }> {} class AuthFehler extends Data.TaggedError("AuthFehler")<{ benutzer?: string grund: "abgelaufen" | "ungueltig" | "gesperrt" }> {} class BestellungFehler extends Data.TaggedError("BestellungFehler")<{ bestellId: string phase: "validierung" | "zahlung" | "versand" grund: string }> {} class ZahlungFehler extends Data.TaggedError("ZahlungFehler")<{ betrag: number anbieter: "stripe" | "paypal" code: string }> {} // Union-Typ für alle App-Fehler type AlleAppFehler = AppFehler | AuthFehler | BestellungFehler | ZahlungFehler // HTTP-Fehler-Mapper const fehlerZuStatusCode = (fehler: AlleAppFehler): number => { switch (fehler._tag) { case "AuthFehler": return 401 case "BestellungFehler": return 422 case "ZahlungFehler": return 402 default: return 500 } }

Effect-TS mit Claude Code produktiv einsetzen

Starte jetzt dein kostenloses Trial und entdecke, wie Claude Code dir beim Aufbau typsicherer Effect-TS-Architekturen hilft — von Layer-Design bis zu Schema-Definitionen.

Kostenloses Trial starten →

ZusammenfassungWas haben wir gelernt?

Effect-TS ist kein kleines Utility-Framework — es ist eine vollständige Neukonzeption, wie TypeScript-Backends gebaut werden. Die wichtigsten Erkenntnisse:

Lernkurve: Effect-TS ist mächtig, aber die Lernkurve ist real. Empfehlung: Starte mit Effect.gen statt pipe, nutze Claude Code als Pair-Programmer und migriere incrementell — nicht alles auf einmal.

#effect-ts #typescript #funktional #claude-code #dependency-injection #fiber #schema #error-handling #2026