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
| Error | When |
|---|---|
| TransactionBuilderError | Build phase failures (insufficient funds, invalid parameters) |
| ProviderError | Provider communication issues (network, API errors) |
| EvaluationError | Plutus 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
| Error | Source | Common Causes |
|---|---|---|
| TransactionBuilderError | Client.newTx().build() | Insufficient funds, invalid parameters, missing required fields |
| EvaluationError | Script evaluation phase | Plutus validator rejected the transaction, exceeded execution limits |
| CoinSelectionError | UTxO selection phase | Wallet has insufficient funds, no UTxOs match required assets |
Provider
| Error | Source | Common Causes |
|---|---|---|
| ProviderError | Any provider call | Network timeout, invalid API key, rate limiting, endpoint down |
Encoding / Decoding
| Error | Source | Common Causes |
|---|---|---|
| DataError | Data.withSchema() codec operations | Schema mismatch, invalid PlutusData structure |
| CBORError | CBOR encoding/decoding | Malformed CBOR bytes, unexpected data format |
| UPLCError | UPLC operations | Invalid flat encoding, corrupted script bytes |
Wallet / Key
| Error | Source | Common Causes |
|---|---|---|
| WalletError | Wallet operations | CIP-30 wallet not connected, user rejected, missing API method |
| DerivationError | Key derivation | Invalid mnemonic, bad derivation path |
| PrivateKeyError | Private key operations | Invalid key bytes, signing failure |
| Bip32PrivateKeyError | BIP-32 HD keys | Invalid extended key |
| Bip32PublicKeyError | BIP-32 public keys | Invalid public key derivation |
Script
| Error | Source | Common Causes |
|---|---|---|
| NativeScriptError | Native script operations | Invalid 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:
| Field | Type | Description |
|---|---|---|
purpose | string | "spend", "mint", "withdraw", or "publish" |
index | number | Index within the purpose category |
label | string? | Your debug label from collectFrom({ label: "..." }) |
validationError | string | The error message from the validator |
traces | string[] | Execution traces emitted by the script (trace calls in Aiken/Plutus) |
scriptHash | string? | Hash of the failed script |
utxoRef | string? | UTxO reference (for spend redeemers) |
policyId | string? | Policy ID (for mint redeemers) |
credential | string? | 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
- Architecture — How errors flow through build phases
- TypeScript Tips — Type patterns with Effect
- UPLC — UPLC error debugging