Skip to main content

Error class hierarchy

The Mandate SDK throws 5 typed error classes. Every error extends MandateError, which extends the native Error. Use instanceof to handle each scenario precisely.
MandateError (base: any API error)
├── PolicyBlockedError   (422: policy rule violated)
├── CircuitBreakerError  (403: agent emergency-stopped)
├── ApprovalRequiredError (202: human approval needed)
└── RiskBlockedError     (422: risk scanner flagged)
All classes are exported from @mandate.md/sdk. Import them in a single statement:
import {
  MandateError,
  PolicyBlockedError,
  CircuitBreakerError,
  ApprovalRequiredError,
  RiskBlockedError,
} from '@mandate.md/sdk';

Catching errors with instanceof

Order matters. Check specific subclasses before the base MandateError. Here is the complete pattern:
import {
  MandateClient,
  PolicyBlockedError,
  CircuitBreakerError,
  ApprovalRequiredError,
  RiskBlockedError,
  MandateError,
} from '@mandate.md/sdk';

const client = new MandateClient({ runtimeKey: process.env.MANDATE_RUNTIME_KEY! });

try {
  await client.validate({
    action: 'transfer',
    amount: '500',
    to: '0xRecipientAddress',
    token: 'USDC',
    reason: 'Payment for API access',
  });
} catch (err) {
  if (err instanceof CircuitBreakerError) {
    // Emergency stop. All transactions blocked until owner resets.
    console.error('EMERGENCY: Circuit breaker active. Halting agent.');
    // Notify ops team. Stop the agent loop. Do NOT retry.
    process.exit(1);

  } else if (err instanceof RiskBlockedError) {
    // Destination address flagged by Aegis risk scanner.
    console.error(`Risky address: ${err.blockReason}`);
    // Do not retry with the same address. May need manual review.

  } else if (err instanceof ApprovalRequiredError) {
    // Transaction needs human sign-off. Poll until decided.
    console.log(`Approval needed: ${err.approvalReason}`);
    const status = await client.waitForApproval(err.intentId, {
      timeoutMs: 3600_000,
      onPoll: (s) => console.log(`Status: ${s.status}`),
    });
    if (status.status === 'approved') {
      console.log('Approved. Proceeding.');
    }

  } else if (err instanceof PolicyBlockedError) {
    // Policy rule violated. Show the decline message.
    console.log(`Blocked: ${err.blockReason}`);
    if (err.declineMessage) console.log(`Message: ${err.declineMessage}`);
    if (err.detail) console.log(`Detail: ${err.detail}`);
    // Do NOT retry with the same parameters.

  } else if (err instanceof MandateError) {
    // Generic API error. Check statusCode.
    console.error(`API error ${err.statusCode}: ${err.message}`);
    // 5xx: safe to retry with backoff. 4xx: fix the request.
  }
}

What to do for each error

ErrorHTTPAction
CircuitBreakerError403Stop all transactions. Notify the agent owner. Wait for dashboard reset.
RiskBlockedError422Verify the destination address. Do not retry with the same address.
ApprovalRequiredError202Call waitForApproval(). See Handle Approvals.
PolicyBlockedError422Display declineMessage. Log blockReason. Adjust params or update policy.
MandateErrorvariesCheck statusCode. Retry 5xx with backoff. Fix 4xx requests.

Block reason reference

When a PolicyBlockedError fires, the blockReason field tells you exactly which check failed. Here are the most common values:
blockReasonMeaningAction
per_tx_limit_exceededSingle transaction amount too highReduce amount or request policy change
daily_quota_exceededDaily spend limit exhaustedWait until the next UTC day or request increase
monthly_quota_exceededMonthly spend limit exhaustedWait until the next month or request increase
address_not_allowedDestination not in allowlistAdd the address in Policy Builder
action_not_allowedAction type blocked by policyUpdate allowed actions in policy
selector_not_allowedFunction selector blockedUpdate allowed selectors in policy
schedule_outside_windowTransaction outside allowed hoursWait for the schedule window to open
circuit_breaker_activeEmergency stop is activeOwner must reset the circuit breaker
reason_blockedPrompt injection detected in reason fieldReview and rewrite the reason text
aegis_critical_riskAddress flagged as critical riskDo not transact with this address
aegis_high_riskAddress flagged as high riskVerify the address before proceeding
See Block Reasons Reference for the full list of all reason codes.

Retry strategy

Not all errors are retryable. Follow these rules to avoid wasting cycles or triggering rate limits.
Error typeRetryable?Strategy
Network error / timeoutYesExponential backoff: 1s, 2s, 4s. Max 3 attempts.
MandateError (5xx)YesExponential backoff: 1s, 2s, 4s. Max 3 attempts.
MandateError (4xx)NoFix the request. Client-side problem.
PolicyBlockedErrorNoSame parameters will fail again. Adjust amount, address, or policy.
ApprovalRequiredErrorN/ANot a failure. Poll with waitForApproval().
CircuitBreakerErrorNoDo not retry until the owner resets via dashboard.
RiskBlockedErrorNoDo not retry with the same destination address.
async function validateWithRetry(client: MandateClient, params: any, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await client.validate(params);
    } catch (err) {
      if (err instanceof MandateError && err.statusCode >= 500 && attempt < maxRetries - 1) {
        await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
        continue;
      }
      throw err; // PolicyBlockedError, CircuitBreakerError, etc. bubble up
    }
  }
}

Fail-safe rules

Non-negotiable fail-safe rules. Every Mandate integration must follow these:
  1. Always validate before signing. Never sign or broadcast a transaction without calling validate() first.
  2. Block if API is unreachable. If the Mandate API returns a network error or timeout, do NOT execute the transaction. Block and retry.
  3. Never ignore errors. If validate() throws, the transaction must not proceed. No fallback to unvalidated execution.
  4. Display the block reason. When a transaction is blocked, show the human-readable declineMessage or blockReason to the user or log.
  5. Post events after broadcast. For raw validation flows, always call postEvent() with the txHash after broadcasting. This enables envelope verification.
If the Mandate API is unreachable, your agent must block the transaction. Never fall back to unvalidated execution. A network failure is not permission to skip validation. Treat an unreachable API the same as a rejection.
This is the most critical rule in any Mandate integration. If you cannot reach the API, do not execute the transaction. Block, log the error, and retry later.

Next Steps

Error Classes Reference

Full property tables and code examples for all 5 error classes.

Block Reasons

Complete list of blockReason codes, meanings, and recommended actions.

Validate Transactions

Step-by-step guide to calling validate() in your agent code.

Common Errors

Troubleshoot frequent issues with error codes and solutions.