Skip to main content
This guide walks you through creating a complete Effect application that demonstrates the core concepts: Effects, services, layers, and error handling.

What You’ll Build

You’ll create a user management service that:
  • Fetches user data with type-safe error handling
  • Uses dependency injection with services and layers
  • Implements structured logging
  • Demonstrates Effect’s generator syntax

Prerequisites

Make sure you’ve installed Effect and have a TypeScript project set up.

Building the Application

1

Define custom errors

First, create typed errors using Schema.TaggedErrorClass. This makes errors part of your type signatures:
index.ts
import { Effect, Schema } from "effect"

// Define a custom error for when a user is not found
export class UserNotFoundError extends Schema.TaggedErrorClass<UserNotFoundError>()("UserNotFoundError", {
  userId: Schema.Number
}) {}

// Define a custom error for database issues
export class DatabaseError extends Schema.TaggedErrorClass<DatabaseError>()("DatabaseError", {
  message: Schema.String,
  cause: Schema.Defect
}) {}
Schema.TaggedErrorClass creates serializable error types that work seamlessly with Effect’s error handling.
2

Create a service

Services encapsulate your application’s capabilities. Define a UserRepository service:
index.ts
import { Effect, Layer, ServiceMap } from "effect"

// Define a User type
interface User {
  readonly id: number
  readonly name: string
  readonly email: string
}

// Create the UserRepository service
export class UserRepository extends ServiceMap.Service<UserRepository, {
  findById(id: number): Effect.Effect<User, UserNotFoundError | DatabaseError>
  list(): Effect.Effect<Array<User>, DatabaseError>
}>()("quickstart/UserRepository") {
  // Define the service implementation as a Layer
  static readonly layer = Layer.effect(
    UserRepository,
    Effect.gen(function*() {
      // Simulate a database with in-memory data
      const users: Map<number, User> = new Map([
        [1, { id: 1, name: "Alice", email: "[email protected]" }],
        [2, { id: 2, name: "Bob", email: "[email protected]" }],
        [3, { id: 3, name: "Charlie", email: "[email protected]" }]
      ])

      // Implement the findById method
      const findById = Effect.fn("UserRepository.findById")(
        function*(id: number): Effect.fn.Return<User, UserNotFoundError | DatabaseError> {
          yield* Effect.log("Looking up user", id)
          
          const user = users.get(id)
          if (!user) {
            return yield* new UserNotFoundError({ userId: id })
          }
          
          return user
        }
      )

      // Implement the list method
      const list = Effect.fn("UserRepository.list")(
        function*(): Effect.fn.Return<Array<User>, DatabaseError> {
          yield* Effect.log("Fetching all users")
          return Array.from(users.values())
        }
      )

      // Return the service implementation
      return UserRepository.of({ findById, list })
    })
  )
}
Effect.fn is used to define functions that return Effects. It provides better stack traces and automatic tracing.
3

Write application logic

Now create your main program that uses the service:
index.ts
// Create a program that fetches and displays a user
const getUserDetails = Effect.fn("getUserDetails")(
  function*(userId: number) {
    // Access the UserRepository service
    const userRepo = yield* UserRepository
    
    yield* Effect.log("Starting user lookup")
    
    // Fetch the user (this can fail with UserNotFoundError or DatabaseError)
    const user = yield* userRepo.findById(userId)
    
    yield* Effect.log("User found:", user.name)
    
    return `User: ${user.name} (${user.email})`
  }
)

// Create a program that lists all users
const listAllUsers = Effect.gen(function*() {
  const userRepo = yield* UserRepository
  
  yield* Effect.log("Fetching user list")
  
  const users = yield* userRepo.list()
  
  yield* Effect.log(`Found ${users.length} users`)
  
  return users
})
4

Add error handling

Handle errors gracefully with Effect’s error handling combinators:
index.ts
// Handle errors and provide fallback behavior
const safeGetUserDetails = getUserDetails(1).pipe(
  // Catch specific error types
  Effect.catchTag("UserNotFoundError", (error) => {
    return Effect.gen(function*() {
      yield* Effect.logWarning("User not found:", error.userId)
      return "User not found"
    })
  }),
  // Catch database errors
  Effect.catchTag("DatabaseError", (error) => {
    return Effect.gen(function*() {
      yield* Effect.logError("Database error:", error.message)
      return "Service temporarily unavailable"
    })
  })
)
Effect.catchTag lets you handle specific error types. The error type is removed from the signature after handling.
5

Compose the full program

Combine everything into a main program:
index.ts
const program = Effect.gen(function*() {
  yield* Effect.log("=== User Management System ===")
  
  // Get a specific user
  const userDetails = yield* safeGetUserDetails
  yield* Effect.log("Result:", userDetails)
  
  // List all users
  const allUsers = yield* listAllUsers
  yield* Effect.log("Total users:", allUsers.length)
  
  // Try to get a non-existent user
  const notFound = yield* getUserDetails(999).pipe(
    Effect.catchTag("UserNotFoundError", () => 
      Effect.succeed("User does not exist")
    )
  )
  yield* Effect.log("Not found result:", notFound)
  
  return "Program completed successfully"
})
6

Provide dependencies and run

Finally, provide the service implementation and run the program:
index.ts
// Provide the UserRepository implementation
const runnable = program.pipe(
  Effect.provide(UserRepository.layer)
)

// Run the program
Effect.runPromise(runnable).then(
  (result) => console.log("\nFinal result:", result),
  (error) => console.error("Program failed:", error)
)
Effect.provide injects the service implementation. Effect.runPromise executes the Effect and returns a Promise.
7

Run your application

Execute your program:
npx tsx index.ts
You should see output like:
timestamp=... level=INFO fiber=#0 message="=== User Management System ==="
timestamp=... level=INFO fiber=#0 message="Starting user lookup"
timestamp=... level=INFO fiber=#0 message="Looking up user" message=1
timestamp=... level=INFO fiber=#0 message="User found:" message=Alice
timestamp=... level=INFO fiber=#0 message=Result: message="User: Alice ([email protected])"
timestamp=... level=INFO fiber=#0 message="Fetching user list"
timestamp=... level=INFO fiber=#0 message="Fetching all users"
timestamp=... level=INFO fiber=#0 message="Found 3 users"
timestamp=... level=INFO fiber=#0 message="Total users:" message=3
...

Final result: Program completed successfully

Complete Example

Here’s the full code in one place:
index.ts
import { Effect, Layer, Schema, ServiceMap } from "effect"

// Define custom errors
export class UserNotFoundError extends Schema.TaggedErrorClass<UserNotFoundError>()("UserNotFoundError", {
  userId: Schema.Number
}) {}

export class DatabaseError extends Schema.TaggedErrorClass<DatabaseError>()("DatabaseError", {
  message: Schema.String,
  cause: Schema.Defect
}) {}

// Define User type
interface User {
  readonly id: number
  readonly name: string
  readonly email: string
}

// Create UserRepository service
export class UserRepository extends ServiceMap.Service<UserRepository, {
  findById(id: number): Effect.Effect<User, UserNotFoundError | DatabaseError>
  list(): Effect.Effect<Array<User>, DatabaseError>
}>()("quickstart/UserRepository") {
  static readonly layer = Layer.effect(
    UserRepository,
    Effect.gen(function*() {
      const users: Map<number, User> = new Map([
        [1, { id: 1, name: "Alice", email: "[email protected]" }],
        [2, { id: 2, name: "Bob", email: "[email protected]" }],
        [3, { id: 3, name: "Charlie", email: "[email protected]" }]
      ])

      const findById = Effect.fn("UserRepository.findById")(
        function*(id: number): Effect.fn.Return<User, UserNotFoundError | DatabaseError> {
          yield* Effect.log("Looking up user", id)
          const user = users.get(id)
          if (!user) {
            return yield* new UserNotFoundError({ userId: id })
          }
          return user
        }
      )

      const list = Effect.fn("UserRepository.list")(
        function*(): Effect.fn.Return<Array<User>, DatabaseError> {
          yield* Effect.log("Fetching all users")
          return Array.from(users.values())
        }
      )

      return UserRepository.of({ findById, list })
    })
  )
}

// Application logic
const getUserDetails = Effect.fn("getUserDetails")(
  function*(userId: number) {
    const userRepo = yield* UserRepository
    yield* Effect.log("Starting user lookup")
    const user = yield* userRepo.findById(userId)
    yield* Effect.log("User found:", user.name)
    return `User: ${user.name} (${user.email})`
  }
)

const listAllUsers = Effect.gen(function*() {
  const userRepo = yield* UserRepository
  yield* Effect.log("Fetching user list")
  const users = yield* userRepo.list()
  yield* Effect.log(`Found ${users.length} users`)
  return users
})

// Error handling
const safeGetUserDetails = getUserDetails(1).pipe(
  Effect.catchTag("UserNotFoundError", (error) => {
    return Effect.gen(function*() {
      yield* Effect.logWarning("User not found:", error.userId)
      return "User not found"
    })
  }),
  Effect.catchTag("DatabaseError", (error) => {
    return Effect.gen(function*() {
      yield* Effect.logError("Database error:", error.message)
      return "Service temporarily unavailable"
    })
  })
)

// Main program
const program = Effect.gen(function*() {
  yield* Effect.log("=== User Management System ===")
  
  const userDetails = yield* safeGetUserDetails
  yield* Effect.log("Result:", userDetails)
  
  const allUsers = yield* listAllUsers
  yield* Effect.log("Total users:", allUsers.length)
  
  const notFound = yield* getUserDetails(999).pipe(
    Effect.catchTag("UserNotFoundError", () => 
      Effect.succeed("User does not exist")
    )
  )
  yield* Effect.log("Not found result:", notFound)
  
  return "Program completed successfully"
})

// Run the program
const runnable = program.pipe(Effect.provide(UserRepository.layer))

Effect.runPromise(runnable).then(
  (result) => console.log("\nFinal result:", result),
  (error) => console.error("Program failed:", error)
)

Key Concepts Demonstrated

This example showcases several important Effect patterns:

Effect.gen & Effect.fn

Use generator functions for imperative-style code with yield* to unwrap Effects.

Services & Layers

Define services with ServiceMap.Service and provide implementations with Layer.effect.

Tagged Errors

Create type-safe errors with Schema.TaggedErrorClass and handle them with Effect.catchTag.

Dependency Injection

Use Effect.provide to inject service implementations, making code testable and modular.

What’s Next?

Now that you’ve built your first Effect application, explore more advanced topics:

Core Concepts

Deep dive into the Effect type and its capabilities

Services & Layers

Master dependency injection and service composition

Error Handling

Learn advanced error handling patterns

Testing

Write tests for your Effect applications

Common Patterns

Running in Node.js with NodeRuntime

For production applications, use NodeRuntime.runMain instead of Effect.runPromise:
import { NodeRuntime } from "@effect/platform-node"

NodeRuntime.runMain(runnable)
This sets up proper signal handling for graceful shutdown.

Creating Effects from Promises

Wrap existing Promise-based APIs:
import { Effect } from "effect"

const fetchData = Effect.tryPromise({
  async try() {
    const response = await fetch("https://api.example.com/data")
    return await response.json()
  },
  catch: (error) => new DatabaseError({ 
    message: "Failed to fetch", 
    cause: error 
  })
})

Testing with Mock Services

Replace real implementations with test doubles:
import { Layer } from "effect"

const TestUserRepository = Layer.effect(
  UserRepository,
  Effect.succeed(
    UserRepository.of({
      findById: () => Effect.succeed({ 
        id: 1, 
        name: "Test User", 
        email: "[email protected]" 
      }),
      list: () => Effect.succeed([])
    })
  )
)

const testRunnable = program.pipe(Effect.provide(TestUserRepository))

Troubleshooting

Make sure you’re using yield* (with asterisk) to unwrap Effects, not just yield. The asterisk is required for TypeScript to infer types correctly.
If you get runtime errors about missing services, ensure you’ve called Effect.provide() with all required service layers before running the program.
Remember that Effect.catchTag only catches errors that are in the Effect’s error channel. Use the correct error tag name (must match the first parameter to TaggedErrorClass).

Build docs developers (and LLMs) love