Effect-TS is the missing standard library for TypeScript. Typed errors, dependency injection, concurrency, retries, and streaming — without external libraries.
What Is Effect?
Effect is a TypeScript library that gives you typed errors, composable services, structured concurrency, and resource management. Think of it as Rust's Result type meets Go's context, but in TypeScript.
Core Concept: Typed Errors
import { Effect, pipe } from 'effect'
class UserNotFound { readonly _tag = 'UserNotFound' as const }
class DatabaseError { readonly _tag = 'DatabaseError' as const }
// This function's type tells you EXACTLY what can go wrong
const getUser = (id: string): Effect.Effect<User, UserNotFound | DatabaseError> =>
Effect.tryPromise({
try: () => db.users.findUnique({ where: { id } }),
catch: () => new DatabaseError(),
}).pipe(
Effect.flatMap((user) =>
user ? Effect.succeed(user) : Effect.fail(new UserNotFound())
)
)
// Handle each error type differently
const program = getUser('123').pipe(
Effect.catchTag('UserNotFound', () => Effect.succeed(defaultUser)),
Effect.catchTag('DatabaseError', (e) => Effect.die(e)), // crash on DB errors
)
Services (Dependency Injection)
import { Context, Layer, Effect } from 'effect'
// Define service interface
class EmailService extends Context.Tag('EmailService')<
EmailService,
{ send: (to: string, body: string) => Effect.Effect<void> }
>() {}
class Logger extends Context.Tag('Logger')<
Logger,
{ log: (msg: string) => Effect.Effect<void> }
>() {}
// Use services
const sendWelcome = (email: string) =>
Effect.gen(function* () {
const emailService = yield* EmailService
const logger = yield* Logger
yield* logger.log(`Sending welcome to ${email}`)
yield* emailService.send(email, 'Welcome!')
})
// Provide implementations
const EmailServiceLive = Layer.succeed(EmailService, {
send: (to, body) => Effect.promise(() => resend.emails.send({ to, subject: 'Welcome', html: body })),
})
const LoggerLive = Layer.succeed(Logger, {
log: (msg) => Effect.sync(() => console.log(msg)),
})
// Run with all dependencies
const MainLive = Layer.merge(EmailServiceLive, LoggerLive)
Effect.runPromise(sendWelcome('alice@example.com').pipe(Effect.provide(MainLive)))
Concurrency
// Run 3 tasks in parallel
const results = yield* Effect.all(
[fetchUser(1), fetchUser(2), fetchUser(3)],
{ concurrency: 3 }
)
// Run with bounded concurrency (max 5 at a time)
const allResults = yield* Effect.forEach(
userIds,
(id) => fetchUser(id),
{ concurrency: 5 }
)
// Race: first to succeed wins
const fastest = yield* Effect.race([
fetchFromCDN(url),
fetchFromOrigin(url),
])
Retries
import { Schedule } from 'effect'
const retryPolicy = Schedule.exponential('100 millis').pipe(
Schedule.compose(Schedule.recurs(3)),
)
const resilientFetch = fetchData.pipe(
Effect.retry(retryPolicy),
Effect.timeout('10 seconds'),
)
Streaming
import { Stream } from 'effect'
const events = Stream.fromAsyncIterable(
eventSource.subscribe('orders'),
() => new Error('Stream failed')
).pipe(
Stream.filter((e) => e.type === 'created'),
Stream.map((e) => ({ orderId: e.id, total: e.amount })),
Stream.grouped(10), // batch 10 events
Stream.mapEffect((batch) => processBatch(batch)),
)
await Stream.runDrain(events)
Why Effect?
- Typed errors: know at compile time what can fail
- Composable: pipe operations together
- Testable: swap implementations via Layers
- Concurrent: structured concurrency primitives
- Resource-safe: automatic cleanup
Building resilient scraping pipelines? Scrapfly + Effect = typed, retryable data extraction. Email spinov001@gmail.com for robust scraping solutions.
Top comments (0)