Skip to main content
voltaire-effect uses Effect’s error model: errors are values in the type signature, not invisible exceptions.

Schema Errors

Schema decode produces ParseError on invalid input:
import * as S from 'effect/Schema'
import * as Address from 'voltaire-effect/primitives/Address'

// Sync - throws ParseError
try {
  S.decodeSync(Address.Hex)('invalid')
} catch (e) {
  // e is ParseError
}

// Effect - returns Effect<AddressType, ParseError>
const result = S.decode(Address.Hex)('invalid')
The error type is in the signature. Handle it with catchTag:
import * as Effect from 'effect/Effect'

import * as A from '@tevm/voltaire/Address'

S.decode(Address.Hex)(input).pipe(
  Effect.catchTag("ParseError", (e) => 
    Effect.succeed(A.zero())
  )
)

Error Details

ParseError contains structured information:
import * as Effect from 'effect/Effect'
import * as S from 'effect/Schema'

S.decode(Address.Hex)('0x123').pipe(
  Effect.catchTag("ParseError", (e) => {
    console.log(e.message)  // Human-readable error
    console.log(e.issue)    // Structured issue tree
    return Effect.fail(e)
  })
)

Either for Branching

Use decodeEither when you want to branch on success/failure:
import * as S from 'effect/Schema'
import * as Either from 'effect/Either'

const result = S.decodeEither(Address.Hex)(input)

if (Either.isRight(result)) {
  const addr = result.right
  // use addr
} else {
  const error = result.left
  // handle error
}

Service Errors

Services can define their own error types:
import * as Effect from 'effect/Effect'
import { getBlock, withTimeout } from 'voltaire-effect'

const program = Effect.gen(function* () {
  // Per-request timeout override
  const block = yield* getBlock({ blockTag: "latest" })
    .pipe(withTimeout("5 seconds"))
  return block
}).pipe(
  Effect.catchTag("ProviderNotFoundError", (e) =>
    Effect.succeed(null)
  ),
  Effect.catchTag("ProviderTimeoutError", (e) =>
    Effect.succeed(null)
  )
)

Error Composition

Errors accumulate in the type signature:
import { getBalance } from 'voltaire-effect'

// Effect<Address, ParseError, never>
const addr = S.decode(Address.Hex)(input)

// Effect<bigint, ParseError | GetBalanceError, ProviderService>
const program = Effect.gen(function* () {
  const a = yield* S.decode(Address.Hex)(input)
  return yield* getBalance(a)
})
Handle each error type explicitly or let them propagate.

Retry with Schedule

Use Effect Schedule for retry behavior:
import * as Schedule from 'effect/Schedule'
import { getBalance, withRetrySchedule } from 'voltaire-effect'

// Exponential backoff with jitter
const retrySchedule = Schedule.exponential("500 millis").pipe(
  Schedule.jittered,
  Schedule.recurs(5)
)

getBalance(addr).pipe(
  withRetrySchedule(retrySchedule)
)

Compared to Base Voltaire

Base Voltaire throws exceptions:
// Base Voltaire - throws
import { Address } from '@tevm/voltaire'

try {
  Address('invalid')
} catch (e) {
  // e is unknown
}
voltaire-effect makes errors typed and composable:
// voltaire-effect - typed errors
S.decode(Address.Hex)('invalid').pipe(
  Effect.catchTag("ParseError", handleParseError),
  Effect.catchTag("TransportError", handleTransportError)
)

Error Types

voltaire-effect defines these error types:
ErrorTagSource
ParseErrorParseErrorSchema decode failures
SignerErrorSignerErrorTransaction signing failures
TransportErrorTransportErrorNetwork/HTTP failures
ProviderResponseErrorProviderResponseErrorInvalid provider response
ProviderNotFoundErrorProviderNotFoundErrorMissing block/tx/receipt
ProviderValidationErrorProviderValidationErrorInvalid input to provider method
ProviderTimeoutErrorProviderTimeoutErrorProvider operation timed out
ProviderStreamErrorProviderStreamErrorProvider stream failures
ProviderReceiptPendingErrorProviderReceiptPendingErrorReceipt not available yet
ProviderConfirmationsPendingErrorProviderConfirmationsPendingErrorConfirmations not met
BlockStreamErrorBlockStreamErrorBlock streaming failures
CryptoErrorCryptoErrorCryptographic operation failures
Provider methods expose method-specific error unions (e.g. GetBalanceError, GetBlockError) that combine TransportError with relevant provider errors.

Either for Partial Failures

Use Either when operations can partially succeed (e.g., multicall batches):
import { Effect, Either, Array } from 'effect'
import { aggregate3 } from 'voltaire-effect'

const program = Effect.gen(function* () {
  const rawResults = yield* aggregate3(calls)
  
  // Transform to Either for ergonomic handling
  const results = rawResults.map((r, i) =>
    r.success
      ? Either.right({ index: i, data: r.returnData })
      : Either.left({ index: i, message: 'Call failed' })
  )
  
  // Exhaustive pattern matching
  const processed = results.map(r =>
    Either.match(r, {
      onLeft: (err) => ({ balance: null, error: err.message }),
      onRight: (ok) => ({ balance: BigInt(ok.data), error: null })
    })
  )
  
  // Type-safe filtering
  const successes = results.filter(Either.isRight).map(r => r.right)
  const failures = results.filter(Either.isLeft).map(r => r.left)
  
  // Or partition in one pass
  const [fails, wins] = Array.partition(results, Either.isLeft)
  
  return { processed, successes, failures }
})

Either vs Effect

Use CaseUse EitherUse Effect
Partial failures (some succeed, some fail)
Batch results with mixed outcomes
Operations that can retry/recover
Async operations with dependencies
Schema validation (sync)decodeEitherdecode

Either API Quick Reference

import { Either } from 'effect'

// Create
const success = Either.right(42)
const failure = Either.left(new Error('oops'))

// Check type
Either.isRight(success) // true
Either.isLeft(failure)  // true

// Pattern match (exhaustive)
Either.match(result, {
  onLeft: (error) => `Error: ${error.message}`,
  onRight: (value) => `Value: ${value}`
})

// Transform
Either.map(success, n => n * 2)           // Either.right(84)
Either.mapLeft(failure, e => e.message)   // Either.left('oops')

// Extract with fallback
Either.getOrElse(failure, () => 0)        // 0
Either.getOrElse(success, () => 0)        // 42

See Also