Skip to main content
Tools and techniques for debugging Effect programs in voltaire-effect.
For comprehensive debugging, see the Effect Debugging Guide.

Inspect Effect Execution

Log Steps

import { Effect } from 'effect'
import { getBlockNumber } from 'voltaire-effect'

const program = Effect.gen(function* () {
  yield* Effect.log('Starting...')

  const block = yield* getBlockNumber()
  yield* Effect.log(`Block: ${block}`)

  return block
})

Log With Context

import { getBalance } from 'voltaire-effect'

Effect.gen(function* () {
  yield* Effect.logInfo('Fetching balance').pipe(
    Effect.annotateLogs({ address, network: 'mainnet' })
  )
  return yield* getBalance(address)
})

Examine Errors

import { Effect, Cause } from 'effect'

const exit = await Effect.runPromiseExit(program)

if (exit._tag === 'Failure') {
  console.log(Cause.pretty(exit.cause))
}

Catch and Log

program.pipe(
  Effect.catchAll((e) => {
    console.error('Error:', e)
    console.error('Tag:', e._tag)
    return Effect.fail(e)
  })
)

Tap Errors Without Handling

program.pipe(
  Effect.tapError((e) => Effect.log(`Error occurred: ${e._tag}`))
)

Schema Parse Errors

Get Detailed Parse Error

import * as S from 'effect/Schema'
import { Effect } from 'effect'

S.decode(Address.Hex)('invalid').pipe(
  Effect.catchTag('ParseError', (e) => {
    console.log('Message:', e.message)
    console.log('Issue:', JSON.stringify(e.issue, null, 2))
    return Effect.fail(e)
  })
)

Pretty Print Parse Error

import { TreeFormatter } from 'effect/ParseResult'

S.decode(Address.Hex)('0x123').pipe(
  Effect.catchTag('ParseError', (e) => {
    console.log(TreeFormatter.formatIssue(e.issue))
    return Effect.fail(e)
  })
)

Trace Execution

Add Spans

import { Effect } from 'effect'
import { getBalance } from 'voltaire-effect'

const fetchBalance = (address: string) =>
  Effect.gen(function* () {
    return yield* getBalance(address)
  }).pipe(Effect.withSpan('fetchBalance', { attributes: { address } }))

const program = Effect.gen(function* () {
  const { balance1, balance2 } = yield* Effect.all({
    balance1: fetchBalance(addr1),
    balance2: fetchBalance(addr2)
  })
  return { balance1, balance2 }
}).pipe(Effect.withSpan('program'))

Debug RPC Calls

Log Transport Requests

import { Effect, Layer } from 'effect'
import { TransportService, HttpTransport } from 'voltaire-effect'

const LoggingTransport = (url: string) =>
  Layer.effect(
    TransportService,
    Effect.gen(function* () {
      const base = yield* TransportService
      return {
        request: <T>(method: string, params?: unknown[]) =>
          Effect.gen(function* () {
            yield* Effect.log(`RPC: ${method}`, { params })
            const start = Date.now()
            const result = yield* base.request<T>(method, params)
            yield* Effect.log(`RPC: ${method} completed in ${Date.now() - start}ms`)
            return result
          })
      }
    })
  ).pipe(Layer.provide(HttpTransport(url)))

Intercept All Requests

const program = Effect.gen(function* () {
  const transport = yield* TransportService
  
  // Wrap with logging
  const loggedRequest = <T>(method: string, params?: unknown[]) =>
    Effect.gen(function* () {
      console.log(`→ ${method}`, params)
      const result = yield* transport.request<T>(method, params)
      console.log(`← ${method}`, result)
      return result
    })
  
  return yield* loggedRequest('eth_blockNumber')
})

Timing

Measure Effect Duration

import { Effect, Duration } from 'effect'

const timed = program.pipe(
  Effect.timed,
  Effect.map(([duration, result]) => {
    console.log(`Took ${Duration.toMillis(duration)}ms`)
    return result
  })
)

Add Timeout With Debug

program.pipe(
  Effect.timeout(Duration.seconds(10)),
  Effect.catchTag('TimeoutException', () => {
    console.error('Operation timed out after 10s')
    return Effect.fail(new Error('Timeout'))
  })
)

Inspect Layer Composition

Check What’s Provided

import { Layer, Context } from 'effect'
import type { GetBalanceError } from 'voltaire-effect'

// TypeScript shows missing requirements
const program: Effect.Effect<bigint, GetBalanceError, ProviderService>

// Compose layers first, then provide once:
const ProviderLayer = Provider.pipe(Layer.provide(HttpTransport(url)))

const provided = program.pipe(Effect.provide(ProviderLayer))
// Effect.Effect<bigint, GetBalanceError, never>
//                                        ↑ no more requirements

Debug Layer Construction

const MyLayer = Layer.effect(
  MyService,
  Effect.gen(function* () {
    yield* Effect.log('Building MyService layer')
    // ... construct service
    return service
  })
)

Common Issues

Missing Layer

Error: “Service not found: ProviderService” Fix: Compose and provide all required layers:
// Compose layers first
const ProviderLayer = Provider.pipe(Layer.provide(HttpTransport(url)))

// Then provide once
program.pipe(Effect.provide(ProviderLayer))

Wrong Layer Order

Layers that depend on other layers must be composed with their dependencies:
// ✓ Correct - compose layers with dependencies
const TransportLayer = HttpTransport(url)
const ProviderLayer = Provider.pipe(Layer.provide(TransportLayer))

const SignerLayer = Signer.Live.pipe(
  Layer.provide(LocalAccount(key)),
  Layer.provide(ProviderLayer)
)

// ✗ Wrong - Provider doesn't need Signer
Provider.pipe(
  Layer.provide(Signer.Live),  // Incorrect direction
  Layer.provide(HttpTransport(url))
)

Forgot to Yield

import { getBlockNumber } from 'voltaire-effect'

// ✗ Returns Effect, doesn't execute
Effect.gen(function* () {
  getBlockNumber()  // Missing yield*!
})

// ✓ Correct
Effect.gen(function* () {
  return yield* getBlockNumber()
})

Mixed Async Styles

// ✗ Don't mix await and yield*
Effect.gen(function* () {
  const result = await somePromise()  // Wrong!
})

// ✓ Wrap promises
Effect.gen(function* () {
  const result = yield* Effect.promise(() => somePromise())
})

Testing Tips

Snapshot Error Output

import { describe, it, expect } from 'vitest'
import { Effect, Exit } from 'effect'

it('produces expected error', async () => {
  const exit = await Effect.runPromiseExit(failingProgram)
  
  if (Exit.isFailure(exit)) {
    expect(exit.cause._tag).toBe('Fail')
    expect(exit.cause.error._tag).toBe('ParseError')
  }
})

Mock With Specific Errors

import { ProviderResponseError, TransportError } from 'voltaire-effect'

const FailingProvider = Layer.succeed(ProviderService, {
  request: (method: string, _params?: unknown[]) => {
    switch (method) {
      case 'eth_blockNumber':
        return Effect.fail(
          new ProviderResponseError({ method: 'eth_blockNumber' }, 'Invalid response')
        )
      case 'eth_getBalance':
        return Effect.fail(
          new TransportError({ code: 429, message: 'Rate limited' })
        )
      default:
        return Effect.succeed(null)
    }
  }
})

const exit = await Effect.runPromiseExit(
  program.pipe(Effect.provide(FailingProvider))
)

See Also