Effect is a powerful TypeScript library for building robust, type-safe applications. Its API for error handling, concurrency, and dependency injection is unlike anything else in the JS ecosystem.
Effect — Type-Safe Error Handling
Effect tracks errors in the type system:
import { Effect, pipe } from "effect"
class NetworkError {
readonly _tag = "NetworkError"
constructor(readonly message: string) {}
}
class ParseError {
readonly _tag = "ParseError"
constructor(readonly input: string) {}
}
// Effect<User, NetworkError | ParseError, never>
const fetchUser = (id: string) =>
pipe(
Effect.tryPromise({
try: () => fetch(`/api/users/${id}`).then(r => r.json()),
catch: (e) => new NetworkError(String(e))
}),
Effect.flatMap((data) =>
typeof data.name === "string"
? Effect.succeed(data as User)
: Effect.fail(new ParseError(JSON.stringify(data)))
)
)
// Handle specific errors
const program = pipe(
fetchUser("123"),
Effect.catchTag("NetworkError", (e) =>
Effect.succeed({ name: "Offline User", cached: true })
)
)
Concurrency with Fibers
Effect provides structured concurrency:
import { Effect, Fiber } from "effect"
// Run tasks in parallel with limit
const fetchAllUsers = (ids: string[]) =>
Effect.forEach(ids, fetchUser, { concurrency: 5 })
// Race — first to complete wins
const fastest = Effect.race(fetchFromCache, fetchFromDB)
// Timeout
const withTimeout = pipe(
fetchUser("123"),
Effect.timeout("5 seconds")
)
// Background fiber
const program = Effect.gen(function* () {
const fiber = yield* Effect.fork(longRunningTask)
yield* doOtherWork()
const result = yield* Fiber.join(fiber)
return result
})
Dependency Injection with Services
Effect has built-in DI — no frameworks needed:
import { Effect, Context, Layer } from "effect"
// Define service interface
class Database extends Context.Tag("Database")<
Database,
{
readonly query: (sql: string) => Effect.Effect<any[]>
readonly insert: (table: string, data: any) => Effect.Effect<void>
}
>() {}
// Use service in program
const getUsers = Effect.gen(function* () {
const db = yield* Database
return yield* db.query("SELECT * FROM users")
})
// Provide implementation
const PostgresLayer = Layer.succeed(Database, {
query: (sql) => Effect.tryPromise(() => pg.query(sql).then(r => r.rows)),
insert: (table, data) => Effect.tryPromise(() => pg.insert(table, data))
})
// Run with real DB
const result = pipe(
getUsers,
Effect.provide(PostgresLayer),
Effect.runPromise
)
Schema — Runtime Validation
import { Schema } from "effect"
const User = Schema.Struct({
id: Schema.Number,
name: Schema.String.pipe(Schema.minLength(2)),
email: Schema.String.pipe(Schema.pattern(/@/)),
role: Schema.Literal("admin", "user", "editor")
})
type User = Schema.Schema.Type<typeof User>
// Parse with Effect error channel
const parseUser = Schema.decodeUnknown(User)
const result = parseUser({ id: 1, name: "A", email: "bad", role: "admin" })
// Effect<User, ParseError>
Streams — Reactive Data Processing
import { Stream, Effect } from "effect"
const processLogs = pipe(
Stream.fromAsyncIterable(readLogFile(), () => new Error("read failed")),
Stream.filter((line) => line.includes("ERROR")),
Stream.map((line) => parseLogLine(line)),
Stream.grouped(100), // Batch every 100
Stream.mapEffect((batch) => sendToElastic(batch), { concurrency: 3 }),
Stream.runDrain
)
Key Takeaways
- Type-safe errors — errors tracked in the type system
- Structured concurrency with fibers, races, timeouts
- Dependency injection without frameworks
- Schema for runtime validation with type inference
- Streams for reactive data processing
- Retry, scheduling, metrics built-in
Explore Effect docs for the complete API.
Building web scrapers or data pipelines? Check out my Apify actors for ready-made solutions, or email spinov001@gmail.com for custom development.
Top comments (0)