Effect provides a more powerful and type-safe alternative to Promises, with built-in error handling, dependency injection, and resource management.
Why Migrate to Effect?
Type-safe errors : Errors are tracked in the type system
Dependency management : Built-in context for dependency injection
Resource safety : Automatic cleanup with scoped resources
Composability : Rich combinators for complex workflows
Interruption : First-class support for cancellation
Basic Conversions
Creating Effects from Promises
Before (Promise)
After (Effect)
const fetchUser = async ( id : string ) : Promise < User > => {
const response = await fetch ( `/api/users/ ${ id } ` )
return response . json ()
}
Use Effect.promise for promises that never reject, or Effect.tryPromise when you need to handle errors.
Error Handling
Before (Promise)
After (Effect)
async function getUserProfile ( id : string ) : Promise < Profile > {
try {
const user = await fetchUser ( id )
const profile = await fetchProfile ( user . profileId )
return profile
} catch ( error ) {
console . error ( "Failed to fetch profile:" , error )
throw new Error ( "Profile not found" )
}
}
Effect errors are typed and tracked. Don’t use console.error - handle errors explicitly using Effect.catchAll, Effect.catchTag, or other error combinators.
Sequential Operations
Before (Promise)
After (Effect)
async function processOrder ( orderId : string ) {
const order = await fetchOrder ( orderId )
const payment = await processPayment ( order )
const shipment = await createShipment ( order , payment )
return shipment
}
Use Effect.gen for sequential operations - it’s similar to async/await but with better type safety and composability.
Parallel Operations
Before (Promise)
After (Effect)
async function getDashboardData ( userId : string ) {
const [ user , orders , notifications ] = await Promise . all ([
fetchUser ( userId ),
fetchOrders ( userId ),
fetchNotifications ( userId )
])
return { user , orders , notifications }
}
Timeouts
Before (Promise)
After (Effect)
const withTimeout = < T >( promise : Promise < T >, ms : number ) : Promise < T > => {
return Promise . race ([
promise ,
new Promise < never >(( _ , reject ) =>
setTimeout (() => reject ( new Error ( "Timeout" )), ms )
)
])
}
const result = await withTimeout ( fetchUser ( "123" ), 5000 )
Retries
Before (Promise)
After (Effect)
async function fetchWithRetry < T >(
fn : () => Promise < T >,
maxRetries : number
) : Promise < T > {
let lastError : unknown
for ( let i = 0 ; i < maxRetries ; i ++ ) {
try {
return await fn ()
} catch ( error ) {
lastError = error
await new Promise ( resolve => setTimeout ( resolve , 1000 * ( i + 1 )))
}
}
throw lastError
}
Resource Management
Before (Promise)
After (Effect)
class Database {
private connection : Connection | null = null
async connect () {
this . connection = await createConnection ()
}
async query ( sql : string ) {
if ( ! this . connection ) throw new Error ( "Not connected" )
return this . connection . query ( sql )
}
async disconnect () {
await this . connection ?. close ()
this . connection = null
}
}
async function runQuery () {
const db = new Database ()
try {
await db . connect ()
return await db . query ( "SELECT * FROM users" )
} finally {
await db . disconnect ()
}
}
Always use Effect.acquireRelease or Effect.scoped for resource management. This ensures cleanup happens even on errors or interruptions.
Running Effects
Before (Promise)
After (Effect)
fetchUser ( "123" )
. then ( user => console . log ( user ))
. catch ( error => console . error ( error ))
// Or with async/await
try {
const user = await fetchUser ( "123" )
console . log ( user )
} catch ( error ) {
console . error ( error )
}
Migration Strategy
Start at the boundaries : Wrap external APIs with Effect first
Convert incrementally : Use Effect.promise to call existing async code
Type your errors : Define error types for better type safety
Use services : Leverage dependency injection for testability
Test thoroughly : Effect code is easier to test with mocked dependencies
You can mix Effect and Promise code during migration. Use Effect.runPromise to convert Effect to Promise when needed.
Common Patterns
Promise.allSettled
Before (Promise)
After (Effect)
const results = await Promise . allSettled ([
fetchUser ( "1" ),
fetchUser ( "2" ),
fetchUser ( "3" )
])
Conditional Execution
Before (Promise)
After (Effect)
const user = await fetchUser ( id )
if ( user . isPremium ) {
return await fetchPremiumContent ()
}
return await fetchBasicContent ()
Next Steps