Effect-TS — Functional Error Handling Cho TypeScript
Effect-TS đưa functional error handling, dependency injection vào TypeScript. Effect type, Schema, Fiber — viết code an toàn, compose được.
Effect-TS là thư viện functional cho TypeScript — giải quyết 3 vấn đề: error handling type-safe, dependency injection, và effect composition.
import { Effect } from 'effect'
// Effect<Success, Error, Requirements>
const program: Effect.Effect<string, Error, never> = Effect.sync(() => {
return 'Hello, Effect!'
})
Effect.runSync(program) // "Hello, Effect!"typescriptKhông throw, không try/catch mất type. Mọi lỗi đều được type annotations.
Vấn Đề try/catch#
// ❌ TypeScript không biết function này throw gì
async function getUser(id: string) {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error('API error')
return res.json()
}
// Dùng:
try {
const user = await getUser('123')
// user: any — mất type
} catch (e) {
// e: unknown — không biết lỗi gì
}typescript// ✅ Effect biết chính xác error type
const getUser = (id: string): Effect.Effect<User, ApiError, never> =>
Effect.tryPromise({
try: () => fetch(`/api/users/${id}`).then(r => r.json()),
catch: () => new ApiError('Failed to fetch'),
})
// Dùng:
const result = Effect.runSync(getUser('123'))
// result: User — type-safe
// Nếu lỗi: compile-time biết là ApiErrortypescriptCore Types#
Effect#
// Effect<Success, Error, Requirements>
type Effect<A, E, R>
// A — kiểu trả về khi thành công
// E — kiểu lỗi
// R — dependencies cần (DI)typescript// never = không có lỗi / không cần DI
Effect<string, never, never> // Luôn thành công, không cần DI
Effect<string, Error, never> // Có thể lỗi, không cần DI
Effect<string, never, Config> // Cần Config service, không lỗi
Effect<string, Error, Config> // Có thể lỗi, cần ConfigtypescriptOption#
import { Option } from 'effect'
// Option = Some | None — thay thế null/undefined
const maybe = Option.some('hello') // Option<string>
const none = Option.none() // Option<never>
Option.match(maybe, {
onNone: () => 'nothing',
onSome: (v) => `got ${v}`,
})typescriptEither#
import { Either } from 'effect'
// Either<Left, Right> — Left = lỗi, Right = thành công
const success: Either.Either<string, number> = Either.right(42)
const failure: Either.Either<string, number> = Either.left('error')typescriptBuilding Effects#
import { Effect } from 'effect'
// sync — không lỗi
const hello = Effect.sync(() => 'Hello')
// Effect<string, never, never>
// try — có thể lỗi
const parse = Effect.try(() => JSON.parse(input))
// Effect<unknown, unknown, never>
// promise
const fetchUser = Effect.promise(() =>
fetch('/api/user').then(r => r.json())
)
// Effect<unknown, never, never>
// tryPromise — promise có thể lỗi
const safeFetch = Effect.tryPromise({
try: () => fetch('/api/user').then(r => r.json()),
catch: (e) => new FetchError(e),
})
// Effect<unknown, FetchError, never>typescriptComposing Effects#
map / flatMap#
const greet = Effect.sync(() => 'World')
.pipe(
Effect.map((name) => `Hello, ${name}!`),
Effect.map((msg) => msg.toUpperCase()),
)
// Effect<string, never, never>typescriptconst program = Effect.sync(() => 'user-1')
.pipe(
Effect.flatMap((id) => fetchUser(id)), // Effect<User, Error, never>
Effect.map((user) => user.name),
)typescriptGen (giống async/await)#
const program = Effect.gen(function* () {
const user = yield* fetchUser('123')
const posts = yield* fetchPosts(user.id)
return { user, posts }
})
// Effect<{ user: User; posts: Post[] }, Error, never>typescriptGiống async/await nhưng type-safe — biết chính xác error type ở mỗi step.
All (song song)#
const [user, posts, comments] = yield* Effect.all([
fetchUser('123'),
fetchPosts('123'),
fetchComments('123'),
], { concurrency: 3 })
// Chạy song song 3 requesttypescriptError Handling#
catchAll / catchTag#
const program = fetchUser('123').pipe(
Effect.catchAll((error) =>
Effect.sync(() => ({ id: 'default', name: 'Guest' }))
),
)
// Catch theo tag (discriminated union)
Effect.catchTag(program, 'NotFound', (error) =>
Effect.sync(() => ({ id: 'guest', name: 'Guest' }))
)typescriptretry#
const program = fetchUser('123').pipe(
Effect.retry({
times: 3,
delay: (attempt) => 1000 * Math.pow(2, attempt), // Exponential backoff
}),
)typescripttimeout#
const program = fetchUser('123').pipe(
Effect.timeout('5 seconds'),
Effect.catchTag('TimeoutException', () =>
Effect.sync(() => fallbackUser)
),
)typescriptDependency Injection#
import { Context, Effect } from 'effect'
// 1. Định nghĩa service
class Database extends Context.Tag('Database')<
Database,
{
query: (sql: string) => Effect.Effect<unknown, DbError, never>
}
>() {}
// 2. Dùng service
const getUser = (id: string) =>
Effect.gen(function* () {
const db = yield* Database // Inject từ context
const user = yield* db.query(`SELECT * FROM users WHERE id = ${id}`)
return user
})
// Effect<unknown, DbError, Database>
// ↑ cần Database service
// 3. Cung cấp implementation
const liveDb = Database.of({
query: (sql) => Effect.tryPromise(() => pool.query(sql)),
})
const program = getUser('123').pipe(
Effect.provideService(Database, liveDb),
)
// Effect<unknown, DbError, never>
// ↑ đã cung cấp DI → nevertypescriptDependency được xử lý ở compile-time và runtime — không cần decorator, không global state.
Schema#
Effect-Schema cho validation + serialization type-safe:
import { Schema } from 'effect'
const UserSchema = Schema.Struct({
id: Schema.Number,
name: Schema.String,
email: Schema.String.pipe(Schema.email()),
age: Schema.Number.pipe(Schema.positive(), Schema.int()),
role: Schema.Literal('admin', 'user'),
})
type User = Schema.Schema.Type<typeof UserSchema>
// { id: number; name: string; email: string; age: number; role: 'admin' | 'user' }
// Decode — validate + parse
const result = Schema.decodeEither(UserSchema)(input)
if (Either.isRight(result)) {
console.log(result.right.name) // ✅ Type-safe
}
// Encode — serialize
const encoded = Schema.encodeSync(UserSchema)(user)typescriptFiber (Concurrency)#
import { Effect, Fiber } from 'effect'
// Fork — chạy concurrent
const fiber = yield* Effect.fork(longRunningTask)
// Làm việc khác...
yield* otherWork()
// Join — đợi kết quả
const result = yield* Fiber.join(fiber)
// Hoặc race
const winner = yield* Effect.race(fetchFromApi, fetchFromCache)typescriptResource Management#
import { Effect } from 'effect'
// Scope — tự động giải phóng resource
const program = Effect.acquireRelease(
Effect.sync(() => openFile('data.txt')), // acquire
(file) => Effect.sync(() => file.close()), // release
).pipe(
Effect.flatMap((file) => file.read()),
)
// Dù thành công hay lỗi → release luôn chạytypescriptPractical Example#
import { Effect, Schema, pipe } from 'effect'
// Schema
const TodoSchema = Schema.Struct({
id: Schema.Number,
title: Schema.String.pipe(Schema.nonEmpty()),
completed: Schema.Boolean,
userId: Schema.Number,
})
type Todo = Schema.Schema.Type<typeof TodoSchema>
// Service
class TodoApi extends Context.Tag('TodoApi')<
TodoApi,
{
getTodo: (id: number) => Effect.Effect<Todo, ApiError, never>
}
>() {}
class ApiError {
readonly _tag = 'ApiError'
constructor(readonly message: string) {}
}
// Implementation
const liveTodoApi = TodoApi.of({
getTodo: (id) =>
Effect.tryPromise({
try: () =>
fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
.then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
}),
catch: (e) => new ApiError(String(e)),
}).pipe(
Effect.flatMap((data) =>
Schema.decodeEither(TodoSchema)(data)
|> Either.mapLeft(() => new ApiError('Invalid response'))
|> Effect.fromEither
),
),
})
// Program
const program = Effect.gen(function* () {
const api = yield* TodoApi
const todo = yield* api.getTodo(1)
return `Todo: ${todo.title} (${todo.completed ? 'done' : 'pending'})`
}).pipe(
Effect.provideService(TodoApi, liveTodoApi),
Effect.retry({ times: 2 }),
Effect.timeout('10 seconds'),
Effect.catchAll((err) =>
Effect.sync(() => `Error: ${err.message}`)
),
)
// Run
const result = Effect.runPromise(program)
// "Todo: delectus aut autem (pending)"typescriptKhi Nào Dùng Effect-TS?#
✅ Nên dùng#
- Backend service — error handling + DI type-safe
- Data pipeline — nhiều step, cần retry/timeout
- Domain-driven design — typed errors là first-class
- Large TypeScript codebase — cần kiểm soát error flow
❌ Không nên#
- Small project — boilerplate không đáng
- Frontend app đơn giản — React Query xử lý được
- Team không quen FP — learning curve cao
- Quick prototype — throw + try/catch nhanh hơn
Tổng Kết#
Effect-TS biến error handling từ runtime thành compile-time concern.
// ❌ Try/catch
function divide(a: number, b: number): number {
if (b === 0) throw new DivideByZeroError()
return a / b
}
// TypeScript: number — nhưng thực tế có thể throw
// ✅ Effect
function divide(a: number, b: number): Effect.Effect<number, DivideByZeroError, never> {
return b === 0
? Effect.fail(new DivideByZeroError())
: Effect.sync(() => a / b)
}
// TypeScript: Effect<number, DivideByZeroError, never> — chính xáctypescript| try/catch | Effect | |
|---|---|---|
| Error type | unknown | Type-safe |
| Composition | Try/catch lồng nhau | .pipe, flatMap, gen |
| Retry | Tự viết | .retry() |
| Timeout | Promise.race | .timeout() |
| DI | Constructor/DI container | Context.Tag |
| Concurrency | Promise.all | Effect.all, Fiber |
| Resource | try/finally | acquireRelease |
| Schema | Zod/zod validation | Schema + Effect |
Effect-TS không phải cho mọi project. Cho backend TypeScript phức tạp, nó thay đổi cách bạn nghĩ về error handling.