Skip to main content
Voltaire uses viem-style errors—synchronous exceptions with rich metadata for debugging. This pattern, pioneered by viem, provides machine-readable error codes alongside human-readable messages.

Why Exceptions?

Voltaire primitives stay close to TypeScript and Ethereum specifications. Since JavaScript’s native APIs throw exceptions (e.g., JSON.parse, new URL), Voltaire follows the same pattern for consistency. However, we recommend wrapping Voltaire in more error-safe patterns for application code:
  • neverthrow — Lightweight Result type for TypeScript
  • Effect.ts — Full-featured typed effects system (Voltaire provides /effect exports)
import { Result, ok, err } from 'neverthrow'
import { Address, InvalidAddressError } from '@tevm/voltaire'

function parseAddress(input: string): Result<AddressType, InvalidAddressError> {
  try {
    return ok(Address(input))
  } catch (error) {
    return err(error as InvalidAddressError)
  }
}

// Now you get type-safe error handling
const result = parseAddress(userInput)
result.match(
  (addr) => console.log('Valid:', addr.toHex()),
  (error) => console.log('Invalid:', error.code)
)

Basic Pattern

Constructors throw when given invalid input:
import { Address, Hex } from '@tevm/voltaire'

// Valid input - returns AddressType
const addr = Address('0x742d35Cc6634C0532925a3b844Bc9e7595f51e3e');

// Invalid input - throws InvalidHexFormatError
Address('not-an-address');

// Invalid length - throws InvalidAddressLengthError
Address('0x1234');
Use try/catch to handle errors:
try {
  const addr = Address(userInput);
  // addr is guaranteed valid here
} catch (error) {
  if (error instanceof InvalidAddressLengthError) {
    console.log(`Expected 20 bytes, got ${error.context?.actualLength}`);
  }
}

Error Hierarchy

All errors extend PrimitiveError:
Error (native)
  └── AbstractError
       └── PrimitiveError
            ├── ValidationError
            │    ├── InvalidFormatError
            │    │    └── InvalidHexFormatError
            │    ├── InvalidLengthError
            │    │    └── InvalidAddressLengthError
            │    ├── InvalidRangeError
            │    │    ├── UintOverflowError
            │    │    └── UintNegativeError
            │    ├── InvalidChecksumError
            │    └── InvalidValueError
            ├── CryptoError
            ├── TransactionError
            └── SerializationError

Error Metadata

Every error includes debugging information:
try {
  Address('0x123');
} catch (error) {
  console.log(error.name);      // "InvalidAddressLengthError"
  console.log(error.code);      // "INVALID_ADDRESS_LENGTH"
  console.log(error.value);     // "0x123"
  console.log(error.expected);  // "20 bytes"
  console.log(error.docsPath);  // "/primitives/address#error-handling"
  console.log(error.context);   // { actualLength: 2 }
  console.log(error.cause);     // Original error if wrapped
}
FieldTypeDescription
namestringError class name
codestringMachine-readable code
valueunknownThe invalid input
expectedstringWhat was expected
docsPathstringLink to documentation
contextobjectAdditional debug info
causeErrorOriginal error (if wrapped)

Validation Functions

Three ways to validate:
import { Address } from '@tevm/voltaire'

// 1. Constructor - throws on invalid
const addr = Address(input);  // Throws InvalidAddressError

// 2. isValid - returns boolean
if (Address.isValid(input)) {
  const addr = Address(input);  // Safe - we know it's valid
}

// 3. assert - throws with custom handling
try {
  const addr = Address.assert(input, { strict: true });
} catch (error) {
  // InvalidAddressError or InvalidChecksumError
}
Choose based on your use case:
  • Constructor: When invalid input is unexpected
  • isValid: When you want to branch on validity
  • assert: When you need validation options (like strict checksum)

Checking Error Types

Use instanceof to check error types:
import {
  InvalidHexFormatError,
  InvalidAddressLengthError,
  InvalidChecksumError
} from '@tevm/voltaire'

try {
  const addr = Address(userInput);
} catch (error) {
  if (error instanceof InvalidHexFormatError) {
    // Not a valid hex string
  } else if (error instanceof InvalidAddressLengthError) {
    // Wrong number of bytes
  } else if (error instanceof InvalidChecksumError) {
    // Failed EIP-55 checksum
  }
}
Or check the code field for simpler handling:
try {
  const addr = Address(userInput);
} catch (error) {
  switch (error.code) {
    case 'INVALID_HEX_FORMAT':
    case 'INVALID_ADDRESS_LENGTH':
    case 'INVALID_CHECKSUM':
      return { error: 'Invalid address' };
    default:
      throw error;  // Unexpected error
  }
}

Effect.ts Integration

For advanced use cases, Voltaire provides Effect.ts schemas:
import { Effect, pipe } from 'effect'
import { AddressSchema } from '@tevm/voltaire/effect'

const program = pipe(
  AddressSchema.decode(userInput),
  Effect.map(addr => addr.toChecksummed()),
  Effect.catchTag('InvalidAddress', (error) =>
    Effect.succeed(`Invalid: ${error.message}`)
  )
)
Effect.ts errors use Data.TaggedError:
import { InvalidAddressLengthError } from '@tevm/voltaire/effect'

// Effect.ts style - tagged errors
const error = new InvalidAddressLengthError({
  value: input,
  actualLength: 10,
  expectedLength: 20
});

error._tag;  // "InvalidAddressLength"
Effect.ts is optional—imported from @tevm/voltaire/effect subpath. Regular usage just throws standard exceptions.

Schema Error Messages

When using Effect schemas, use formatError from effect/ParseResult to get human-readable error messages:
import * as S from "effect/Schema"
import { formatError } from "effect/ParseResult"
import * as Address from 'voltaire-effect/primitives/Address'

// Test error output
const result = S.decodeUnknownEither(Address.Hex)("0x1234")
if (result._tag === "Left") {
  const formatted = formatError(result.left)
  // "Address.Hex
  //  └─ Invalid Ethereum address: expected 40 hex characters, got 4"
}

Error Message Guidelines

When implementing schema annotations, follow these guidelines:
GuidelineExample
✅ Include expected format/length"Expected 40 hex characters"
✅ Include what was received"got 4 characters"
❌ Never include actual value for sensitive typesPrivateKey, Mnemonic
❌ Avoid raw predicate failures"Predicate refinement failure"

Custom Message Annotations

Use .annotations({ message }) to provide helpful error messages:
import * as S from "effect/Schema"

const AddressHex = S.String.pipe(
  S.filter((s) => /^0x[a-fA-F0-9]{40}$/.test(s))
).annotations({
  identifier: "Address.Hex",
  title: "Ethereum Address",
  message: (issue) => {
    const actual = issue.actual
    if (typeof actual !== "string") {
      return `Expected string, got ${typeof actual}`
    }
    if (!actual.startsWith("0x")) {
      return `Address must start with 0x prefix`
    }
    const hexPart = actual.slice(2)
    return `Invalid address: expected 40 hex characters, got ${hexPart.length}`
  }
})

formatError API

The formatError function from effect/ParseResult renders parse errors as human-readable trees:
import { formatError, formatErrorSync } from "effect/ParseResult"
import * as S from "effect/Schema"

// Sync version for Either results
const result = S.decodeUnknownEither(MySchema)(input)
if (result._tag === "Left") {
  console.log(formatError(result.left))
}

// Async version for Effect failures
import { Effect } from "effect"
const program = S.decode(MySchema)(input).pipe(
  Effect.catchAll((error) => {
    console.log(formatError(error))
    return Effect.fail(error)
  })
)
Output format is a tree showing the path to the error:
Address.Hex
└─ Predicate refinement failure
   └─ Invalid address: expected 40 hex characters, got 4

Testing Schema Errors

Iterate on schema error messages to ensure good UX:
import { describe, it, expect } from "vitest"
import * as S from "effect/Schema"
import { formatError } from "effect/ParseResult"
import * as Address from "voltaire-effect/primitives/Address"

describe("Address.Hex error messages", () => {
  it("shows helpful error for short input", () => {
    const result = S.decodeUnknownEither(Address.Hex)("0x1234")
    expect(result._tag).toBe("Left")
    if (result._tag === "Left") {
      const formatted = formatError(result.left)
      expect(formatted).toContain("expected 40 hex characters")
      expect(formatted).toContain("got 4")
    }
  })

  it("shows helpful error for missing prefix", () => {
    const result = S.decodeUnknownEither(Address.Hex)("1234abcd")
    expect(result._tag).toBe("Left")
    if (result._tag === "Left") {
      const formatted = formatError(result.left)
      expect(formatted).toContain("0x prefix")
    }
  })

  it("shows helpful error for wrong type", () => {
    const result = S.decodeUnknownEither(Address.Hex)(12345)
    expect(result._tag).toBe("Left")
    if (result._tag === "Left") {
      const formatted = formatError(result.left)
      expect(formatted).toContain("Expected string")
    }
  })
})

Sensitive Types

Never expose actual values in error messages for sensitive types:
// ❌ BAD - exposes private key
message: (issue) => `Invalid private key: ${issue.actual}`

// ✅ GOOD - describes the problem without exposing data
message: () => "Invalid private key: expected 32 bytes (64 hex characters)"
Sensitive types include:
  • PrivateKey
  • Mnemonic
  • SeedPhrase
  • Any key material

Learn More