Error Handling
Recovery patterns for failed and expired Andamio transactions
Error Handling
How to handle errors and recover from failed transactions.
State Machine States
Transactions can reach three terminal states:
| State | Meaning | Recovery |
|---|---|---|
updated | Success — DB synced | None needed |
failed | DB sync failed after retries | Usually auto-heals; see below |
expired | Not confirmed on-chain in time | Rebuild and resubmit |
Common API Errors
| Code | Meaning | Resolution |
|---|---|---|
400 | Bad request / validation error | Check request body against API Reference |
401 | Invalid or expired JWT | Re-authenticate user via wallet signature |
403 | Insufficient permissions | User lacks required role (e.g., not a teacher) |
409 | Duplicate registration | Transaction already registered — safe to ignore |
422 | Unprocessable entity | Semantic error (e.g., module doesn't exist) |
500 | Internal server error | Retry with exponential backoff |
502 | Upstream service error | Cardano node issue — retry after delay |
Five Failure Modes
Every transaction failure falls into one of five categories, each with a distinct recovery action:
| # | Failure | How to Detect | Recovery |
|---|---|---|---|
| 1 | Build error | HTTP 4xx from tx build | Fix request body, retry build |
| 2 | Sign error | Local error from tx sign | Fix key path or permissions, retry sign |
| 3 | Submit rejection | Cardano rejects the transaction | Fix the condition that caused rejection, rebuild fresh |
| 4 | Chain expiry | State = expired, confirmed_at is null | Check Andamioscan first, then rebuild if truly not on-chain |
| 5 | Off-chain sync failure | State = failed, confirmed_at is set | Do NOT retry. The chain TX succeeded. Off-chain sync will auto-retry. |
Never rebuild a transaction in failed state. If confirmed_at has a value, the transaction is on-chain. Rebuilding would create a duplicate action. The gateway's self-healing reconciler retries the off-chain sync automatically.
Build and Sign Errors (1-2)
These are pre-submission errors — no on-chain state was created. Fix the input and retry safely.
Submit Rejection (3)
Cardano rejected the transaction (e.g., insufficient funds, invalid inputs, script failure). The old unsigned TX is now stale — you must rebuild from scratch because UTxO inputs may have changed.
Commitment State Expiry
When a transaction expires, most commitment states revert to their previous value:
| State Before TX | Reverts To | Notes |
|---|---|---|
SUBMIT | Pre-submission state | Safe revert |
ASSESS | SUBMITTED | Safe revert |
CLAIM | ACCEPTED | Safe revert |
PENDING_TX_COMMIT | No revert | No earlier state exists — this was the first enrollment |
PENDING_TX_LEAVE | No revert | Reverting would undo an intentional user action |
The two non-reverting states require special handling:
- PENDING_TX_COMMIT: The commitment record stays in pending state. Your app must let the user retry the transaction or delete the commitment.
- PENDING_TX_LEAVE: The commitment record stays in pending state. The user intended to leave — don't automatically re-enroll them.
Recovery Patterns
Expired Transaction
A transaction expires when it's not confirmed on-chain within the timeout window (typically 2 hours).
Before rebuilding, check if it actually confirmed:
async function handleExpired(txHash: string) {
// Check Andamioscan to see if TX actually confirmed
const scanRes = await fetch(
`https://preprod.andamioscan.io/api/tx/${txHash}`
);
if (scanRes.ok) {
// TX is on-chain! State machine missed it.
// The self-healing reconciler will fix this on next read.
console.log("TX confirmed but not detected — will auto-heal");
return { action: "wait", reason: "auto-heal" };
}
// TX truly didn't confirm — safe to rebuild
return { action: "rebuild", reason: "not-on-chain" };
}Why this matters: If you rebuild without checking, you might submit a duplicate action (e.g., double-minting a credential).
Failed DB Sync
A failed state means the transaction confirmed on-chain but the database update failed after multiple retries.
Usually no action needed:
The Gateway has a self-healing reconciler that detects and repairs inconsistencies when related data is accessed. Your next API call that reads this data will trigger the repair.
If time-sensitive:
// Force a read to trigger reconciliation
await fetch(`${API}/course/${courseId}/commitments`, { headers });
// The reconciler will notice the on-chain state and sync itNetwork Errors
For transient network issues, implement retry with exponential backoff:
async function fetchWithRetry(
url: string,
options: RequestInit,
maxRetries = 3
) {
for (let i = 0; i < maxRetries; i++) {
try {
const res = await fetch(url, options);
if (res.ok || res.status < 500) return res;
} catch (e) {
if (i === maxRetries - 1) throw e;
}
// Exponential backoff: 1s, 2s, 4s
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
}
}Wallet Errors
Common wallet-related issues:
| Error | Cause | Resolution |
|---|---|---|
| User rejected | User cancelled signing | Prompt to try again |
| Insufficient funds | Not enough ADA | Show required amount |
| TX too large | Too many inputs/outputs | Contact support |
| Collateral not set | dApp collateral not configured | Prompt user to set collateral in wallet |
try {
const signedTx = await wallet.signTx(unsignedTx);
} catch (e) {
if (e.message?.includes("user declined")) {
// User cancelled — don't retry automatically
return { error: "cancelled", retry: false };
}
if (e.message?.includes("insufficient")) {
const required = parseRequiredAda(e.message);
return { error: "insufficient_funds", required };
}
throw e; // Unknown error
}Idempotency
Andamio's state machine is designed for safe retries:
- Registration is idempotent: Registering the same
tx_hashtwice returns409 Conflictbut doesn't create duplicate records - DB syncs are idempotent: If a sync is interrupted and retried, it produces the same result
- Draft upserts: Creating a commitment draft that already exists updates it rather than duplicating
This means if you're unsure whether an operation completed, it's safe to retry.
Debugging
Check Transaction Status
curl -H "X-API-Key: $API_KEY" \
https://preprod.api.andamio.io/api/v2/tx/status/$TX_HASHView Pending Transactions
curl -H "X-API-Key: $API_KEY" \
-H "Authorization: Bearer $JWT" \
https://preprod.api.andamio.io/api/v2/tx/pendingCheck On-Chain State
For preprod:
curl https://preprod.andamioscan.io/api/tx/$TX_HASHFor mainnet:
curl https://andamioscan.io/api/tx/$TX_HASHBest Practices
- Always check before rebuilding expired TXs — Avoid duplicate on-chain actions
- Implement SSE for real-time updates — Better UX than polling
- Store
tx_hashimmediately after submit — Enables recovery if registration fails - Use draft-first flow — Provides instant feedback and safer recovery
- Log state transitions — Helps debug issues in production
Next Steps
- API Concepts — Safe refetch timing and the source field
- API Integration — Complete transaction flow
- Transaction State Machine — State transition details
- API Reference — Endpoint documentation