Error Handling

Every backend failure is normalized into a typed BackendError subclass with a stable code. Branch on code, not HTTP status so multiple distinct conditions share the same status.

Class Hierarchy

BackendError                            // Base - carries statusCode, code, backendName, originalError, target
├── NotFoundError                       // 404 - transaction / address not found
├── TransactionValidationError          // 400 - bad CBOR / inputs / signature
├── ScriptValidationError               // 400 - Plutus script failed
├── TransactionAlreadySubmittedError    // 409 - idempotent duplicate
├── InsufficientFundsError              // 400 - not enough funds in wallet
├── MixedAssetsError                    // 400 - non-ADA UTxO in ADA-only path
├── ProviderUnavailableError            // 503 - timeout / 5xx / WebSocket drop
├── RateLimitError                      // 429 - may include retry-after
├── AllBackendsFailedError              // 503 - every backend failed (carries error[])
├── BackendInitError                    // 500 - init failed
└── HsmError                            // 500/503 - no hsm module found 

ConfigError                             // 500 - bad config (no parent BackendError)
AllBackendsInitFailedError              // 500 - startup fatal

Stable Codes (srv/utils/error-codes.ts)

CodeHTTPMeaning
ODATANO_INVALID_INPUT400Bad bech32 / hash, missing field, JSON over limit
ODATANO_NOT_FOUND404Resource doesn’t exist on chain
ODATANO_INSUFFICIENT_FUNDS400No funds in wallet to build transaction
ODATANO_TX_VALIDATION_FAILED400Signature / witness / CBOR invalid
ODATANO_SCRIPT_VALIDATION_FAILED400Plutus script failed
ODATANO_TX_ALREADY_SUBMITTED409In mempool / on chain
ODATANO_PROVIDER_RATE_LIMITED429Blockfrost / Koios Rate Limt
ODATANO_PROVIDER_UNAVAILABLE503Backend timeout or 5xx
ODATANO_INTERNAL_ERROR500Last-resort fallback
ODATANO_HSM_UNAVAILABLE503HSM device / session not available
ODATANO_HSM_SIGNING_FAILED500Signing with HSM not available
ODATANO_HSM_NOT_CONFIGURED400HSM action called but HSM_ENABLED=false

Backends

BackendBehavior
BlockfrostCorrect HTTP statuses: 404 maps directly
KoiosMay return 5xx for not-found: Text normalization rescues
OgmiosWebSocket drops surface as ProviderUnavailableError

Server-Side Helpers

HelperPurpose
handleRequest(req, async (db) => {...})Service-level wrapper opens cds.tx, maps errors to req.reject
handleBackendRequest(fn, backendName)Backend-call wrapper runs normalizeBackendError()
mapError(req, err, ctx)Manual error → req.reject
rejectMissing(req, ctx, field)400 for missing required field
rejectInvalid(req, ctx, message, target)400 for invalid input
throwIfValidationErrors(req, ctx, errors)Batch throws first error from validateTransactionInputs()

Best Practices

  • Validate inputs before handleRequest(), never inside.
  • Throw typed classes never bare new Error().
  • Wrap backend calls with handleBackendRequest() for pre-normalization.
  • Client-side: branch on code, not status.

Example Response

{ "error": {
  "code": "ODATANO_INSUFFICIENT_FUNDS",
  "message": "Sender 'addr_test1qq…' has 4 500 000 lovelace, needs 10 200 000",
  "target": "senderAddress"
} }

See Also