Skip to content

Conversation

@ricardo-valero
Copy link

@ricardo-valero ricardo-valero commented Oct 27, 2025

Type

  • Refactor
  • Feature
  • Bug Fix
  • Optimization
  • Documentation Update

Description

Unify sync/try and promise/tryPromise APIs

Motivation

Currently, Effect provides four separate functions for wrapping computations:

  • Effect.sync() and Effect.try() for synchronous operations
  • Effect.promise() and Effect.tryPromise() for asynchronous operations

However, usage patterns in the codebase reveal that:

  • Effect.try() is used exclusively with the {try, catch} form (no instances found using the callback-only form)
  • Effect.tryPromise() is used exclusively with the {try, catch} form (no instances found using the callback-only form)

This indicates an opportunity to simplify the API by consolidating these functions.

Proposed Changes

Unify synchronous operations into Effect.sync():

// Operations that don't throw
const log = (message: string) => Effect.sync(() => console.log(message))

// Operations that may throw - with default error handling
const parse = (input: string) => Effect.sync({ try: () => JSON.parse(input) })

// Operations that may throw - with custom error handling
const parse = (input: string) =>
  Effect.sync({
    try: () => JSON.parse(input),
    catch: (unknown) => new ParseError({ cause: unknown })
  })

Unify asynchronous operations into Effect.promise():

// Promises that don't reject
const delay = (message: string) =>
  Effect.promise<string>(() => new Promise((resolve) => setTimeout(() => resolve(message), 2000)))

// Promises that may reject - with default error handling
const getTodo = (id: number) =>
  Effect.promise({
    try: () => fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
  })

// Promises that may reject - with custom error handling
const getTodo = (id: number) =>
  Effect.promise({
    try: () => fetch(`https://jsonplaceholder.typicode.com/todos/${id}`),
    catch: (unknown) => new FetchError({ cause: unknown })
  })

Benefits

  1. Eliminates naming redundancy: Avoids the awkward repetition in Effect.try({ try: ... }) and Effect.tryPromise({ try: ... })
  2. Simpler mental model and reduced API surface: One function per operation type instead of two, users don't need to decide between sync/try or promise/tryPromise they just reach for the operation type they need

Breaking Changes

This change is designed for Effect v4 and requires migration:

Current API New API
Effect.try(() => ...) Effect.sync({try: () => ...})
Effect.try({try, catch}) Effect.sync({try, catch})
Effect.tryPromise(() => ...) Effect.promise({try: () => ...})
Effect.tryPromise({try, catch}) Effect.promise({try, catch})

Since the {try, catch} form is universally used in practice, this consolidation aligns the API with actual usage patterns.

@ricardo-valero ricardo-valero changed the title Refactor/unify promise trypromise Refactor: Unify sync/try and promise/tryPromise APIs Oct 27, 2025
@tim-smart
Copy link
Collaborator

Effect.sync / Effect.promise put errors into the defect channel. What's the equivalent for those after this change?

@ricardo-valero
Copy link
Author

ricardo-valero commented Oct 27, 2025

Both use cases are preserved and remain unchanged.

Effect.sync(() => ...) and Effect.promise(() => ...) would behave the same way and put errors into the defect channel.

Effect.try (to be renamed Effect.sync): No try-catch → throws bubble to FiberImpl.runLoop → exitDie(error) → defect channel

Effect.promise: rescue ? rescue(e) : die(e) → no rescue → defect channel

I added some tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants