Evolution SDK
Advanced

Error Handling

Effect-based error handling patterns

Error Handling

Evolution SDK is built on Effect, providing structured error handling with typed errors. Every operation that can fail returns an Effect<Success, Error> type, making error cases explicit and composable.

Error Types

ErrorWhen
TransactionBuilderErrorBuild phase failures (insufficient funds, invalid parameters)
ProviderErrorProvider communication issues (network, API errors)
EvaluationErrorPlutus script evaluation failures

Build Modes

Evolution SDK offers three ways to handle errors from .build():

The standard .build() returns a Promise. Errors throw as exceptions:

import { , , ,  } from "@evolution-sdk/evolution"

const  = .()
  .({
    : "https://cardano-preprod.blockfrost.io/api/v0",
    : ..!
  })
  .({ : ..!, : 0 })

try {
  const  = await 
    .()
    .({
      : .("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"),
      : .(2_000_000n)
    })
    .()

  const  = await .()
  await .()
} catch () {
  .("Transaction failed:", )
}

Use .buildEffect() for composable error handling with Effect:

import {  } from "effect"
import { , , ,  } from "@evolution-sdk/evolution"

const  = .()
  .({
    : "https://cardano-preprod.blockfrost.io/api/v0",
    : ..!
  })
  .({ : ..!, : 0 })

// Build returns an Effect — errors are values, not exceptions
const  = 
  .()
  .({
    : .("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"),
    : .(2_000_000n)
  })
  .()

// Run with error handling
// Effect.runPromise(program).catch(console.error)

Use .buildEither() for explicit success/failure without exceptions:

import { , , ,  } from "@evolution-sdk/evolution"

const  = .()
  .({
    : "https://cardano-preprod.blockfrost.io/api/v0",
    : ..!
  })
  .({ : ..!, : 0 })

const  = await 
  .()
  .({
    : .("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"),
    : .(2_000_000n)
  })
  .()

// result is Either<Error, SignBuilder>

All Error Types

Evolution SDK uses tagged errors throughout. Here is the complete reference:

Transaction Building

ErrorSourceCommon Causes
TransactionBuilderErrorClient.newTx().build()Insufficient funds, invalid parameters, missing required fields
EvaluationErrorScript evaluation phasePlutus validator rejected the transaction, exceeded execution limits
CoinSelectionErrorUTxO selection phaseWallet has insufficient funds, no UTxOs match required assets

Provider

ErrorSourceCommon Causes
ProviderErrorAny provider callNetwork timeout, invalid API key, rate limiting, endpoint down

Encoding / Decoding

ErrorSourceCommon Causes
DataErrorData.withSchema() codec operationsSchema mismatch, invalid PlutusData structure
CBORErrorCBOR encoding/decodingMalformed CBOR bytes, unexpected data format
UPLCErrorUPLC operationsInvalid flat encoding, corrupted script bytes

Wallet / Key

ErrorSourceCommon Causes
WalletErrorWallet operationsCIP-30 wallet not connected, user rejected, missing API method
DerivationErrorKey derivationInvalid mnemonic, bad derivation path
PrivateKeyErrorPrivate key operationsInvalid key bytes, signing failure
Bip32PrivateKeyErrorBIP-32 HD keysInvalid extended key
Bip32PublicKeyErrorBIP-32 public keysInvalid public key derivation

Script

ErrorSourceCommon Causes
NativeScriptErrorNative script operationsInvalid timelock, bad multi-sig configuration

Debugging Common Errors

Jump to: CoinSelectionError | EvaluationError | ProviderError | CBORError

"Insufficient funds" (CoinSelectionError)

Your wallet doesn't have enough ADA or tokens for the transaction:

try {
  const tx = await client.newTx()
    .payToAddress({ address, assets: Assets.fromLovelace(1000_000_000n) })
    .build()
} catch (e) {
  // Check: Does the wallet have enough ADA?
  // Check: Are UTxOs locked at script addresses?
  // Check: Is there enough ADA for fees + min UTxO?
  console.error(e)
}

Fix: Query your wallet UTxOs first to verify available balance.

"Script evaluation failed" (EvaluationError)

A Plutus validator rejected the transaction:

try {
  const tx = await client.newTx()
    .collectFrom({ inputs: scriptUtxos, redeemer: Data.constr(0n, []) })
    .attachScript({ script: validatorScript })
    .build()
} catch (e) {
  // EvaluationError includes `failures` array with details
  // Check: Is the redeemer correct for your validator?
  // Check: Does the datum match what the validator expects?
  // Check: Are required signers included?
  // Check: Is the validity interval set correctly for time-locked scripts?
  console.error(e)
}

Fix: Use debug labels (label: "my-operation") on collectFrom to identify which script failed. Check your redeemer matches the validator's expected action.

"Provider request failed" (ProviderError)

Network or API issues:

try {
  const tx = await client.newTx()
    .payToAddress({ address, assets })
    .build()
} catch (e) {
  // Check: Is your API key valid?
  // Check: Is the provider endpoint reachable?
  // Check: Are you on the correct network (preprod vs mainnet)?
  console.error(e)
}

Fix: Verify your provider configuration, API key, and network connectivity.

"CBOR decoding failed" (CBORError)

Malformed binary data:

import { CBOR } from "@evolution-sdk/evolution"

try {
  const value = CBOR.fromCBORHex("invalid-hex")
} catch (e) {
  // Check: Is the hex string valid?
  // Check: Is the data actually CBOR-encoded?
  // Check: Are you using the correct encoding level (single vs double CBOR)?
  console.error(e)
}

Fix: Verify the hex string is valid and the correct encoding level. Use UPLC.getCborEncodingLevel() to check script encoding.

Inspecting Errors

All Evolution SDK errors are tagged errors with structured fields. You can inspect them by checking the _tag property.

Identifying Error Types

try {
  const tx = await client.newTx()
    .payToAddress({ address, assets })
    .build()
  const signed = await tx.sign()
  await signed.submit()
} catch (e: any) {
  switch (e._tag) {
    case "TransactionBuilderError":
      console.error("Build failed:", e.message)
      break
    case "EvaluationError":
      console.error("Script failed:", e.message)
      // Inspect individual script failures
      if (e.failures) {
        for (const f of e.failures) {
          console.error(`  [${f.purpose}] ${f.label ?? "unlabeled"}: ${f.validationError}`)
          if (f.traces.length > 0) {
            console.error("  Traces:", f.traces.join(", "))
          }
        }
      }
      break
    case "CoinSelectionError":
      console.error("Insufficient funds:", e.message)
      break
    case "ProviderError":
      console.error("Provider issue:", e.message)
      break
    default:
      console.error("Unknown error:", e)
  }
}

EvaluationError Script Failures

When a Plutus script fails, EvaluationError contains a failures array with detailed information about each failed script:

FieldTypeDescription
purposestring"spend", "mint", "withdraw", or "publish"
indexnumberIndex within the purpose category
labelstring?Your debug label from collectFrom({ label: "..." })
validationErrorstringThe error message from the validator
tracesstring[]Execution traces emitted by the script (trace calls in Aiken/Plutus)
scriptHashstring?Hash of the failed script
utxoRefstring?UTxO reference (for spend redeemers)
policyIdstring?Policy ID (for mint redeemers)
credentialstring?Credential hash (for withdraw/cert redeemers)

Using Labels for Debugging

Add labels to your operations so script failures are easy to identify:

const tx = await client
  .newTx()
  .collectFrom({
    inputs: escrowUtxos,
    redeemer: Data.constr(0n, []),
    label: "claim-escrow"   // This appears in EvaluationError.failures[].label
  })
  .collectFrom({
    inputs: vestingUtxos,
    redeemer: Data.constr(1n, []),
    label: "unlock-vesting"  // Different label for each operation
  })
  .attachScript({ script: escrowScript })
  .attachScript({ script: vestingScript })
  .build()

When a script fails, the error tells you exactly which operation caused it:

EvaluationError: Script evaluation failed
  [spend] claim-escrow: Validator returned False
  Traces: "deadline not reached", "current time: 1735600000"

Safe Parsing with Either

For non-throwing error handling, use buildEither():

const result = await client
  .newTx()
  .payToAddress({ address, assets })
  .buildEither()

if (result._tag === "Left") {
  // result.left is the error — inspect with _tag
  console.error("Failed:", result.left)
} else {
  // result.right is the SignBuilder
  const signed = await result.right.sign()
  await signed.submit()
}

Next Steps

On this page