Error Handling

Every exception the SDK raises, what triggers it, and how to recover

Error Handling

Both SDKs raise the same ten exceptions with the same names and the same inheritance. Everything on this page applies to Python and TypeScript alike unless a row says otherwise.

Exception hierarchy

ArmorIQException (base)
├── ConfigurationException
├── InvalidTokenException
│   └── TokenExpiredException
├── IntentMismatchException
├── MCPInvocationException
├── DelegationException
├── PolicyBlockedException
├── PolicyHoldException
└── RevokedTokenError

Only TokenExpiredException is nested: it extends InvalidTokenException, so except InvalidTokenException also catches expiry. Every other class descends directly from ArmorIQException.

All ten are importable from the package root.

from armoriq_sdk import (
    ArmorIQException,
    ConfigurationException,
    DelegationException,
    IntentMismatchException,
    InvalidTokenException,
    MCPInvocationException,
    PolicyBlockedException,
    PolicyHoldException,
    RevokedTokenError,
    TokenExpiredException,
)

from armoriq_sdk.exceptions import ... also works and is equivalent.

import {
  ArmorIQException,
  ConfigurationException,
  DelegationException,
  IntentMismatchException,
  InvalidTokenException,
  MCPInvocationException,
  PolicyBlockedException,
  PolicyHoldException,
  RevokedTokenError,
  TokenExpiredException,
} from '@armoriq/sdk';

What each one means

ExceptionRaised whenRecovery
ConfigurationExceptionNo API key or a malformed one at construction, or the proxy rejected the key as invalid/revoked during the startup checkSet ARMORIQ_API_KEY, or pass api_key / apiKey explicitly. Check the key in the API Keys dashboard
InvalidTokenExceptionThe proxy rejected the token with 401 or 403 (bad signature, revoked, malformed)Capture a fresh plan and mint a new token
TokenExpiredExceptionThe token is past its validity window. Raised client-side before the request goes out, so it costs no round tripCall get_intent_token / getIntentToken again on the same plan
IntentMismatchExceptionThe proxy returned 409: the action you invoked is not in the plan the token was minted forAlign the arguments with the plan, or capture a new plan that includes the action
MCPInvocationExceptionAny other non-2xx from the proxy, or the MCP server itself erroredInspect status_code / statusCode. Not always an ArmorIQ problem: a 5xx here usually means the MCP is down
PolicyBlockedExceptionPolicy evaluation returned a deny for this user and toolUpdate the policy in the dashboard. Do not retry: the answer will not change
PolicyHoldExceptionPolicy requires human approval before the tool can runWait on the approval, then retry. See holds and approvals
DelegationExceptionA delegation call failed: target unavailable, trust rejected, or a malformed subtaskInspect delegation_id / delegationId and the delegation status
RevokedTokenErrorReserved. See the note belown/a

Attributes

Every exception carries the message. These also carry structured fields, which are more useful than parsing the message string.

ExceptionPythonTypeScript
InvalidTokenExceptiontoken_idtokenId
TokenExpiredExceptiontoken_id, expired_attokenId, expiredAt
IntentMismatchExceptionaction, plan_hashaction, planHash
MCPInvocationExceptionmcp, action, status_codemcp, action, statusCode
DelegationExceptiontarget_agent, delegation_id, status_codetargetAgent, delegationId, statusCode
PolicyBlockedExceptionenforcement_action, reason, metadataenforcementAction, reason, metadata, matchedPolicy
PolicyHoldExceptiondelegation_context, metadatadelegationContext, metadata, matchedPolicy
RevokedTokenErrortoken_id, reasontokenId, reason
ConfigurationExceptionmessage onlymessage only

matchedPolicy on the two policy exceptions is TypeScript-only. The Python equivalents do not carry it yet, so do not rely on it in cross-language code.

Failures raise, they do not come back in the result

There is no success flag to check. A failed invoke() raises; a successful one returns an MCPInvocationResult.

The one exception is a tool that ran but reported its own error. When the MCP returns an error payload rather than failing the transport, invoke() returns normally with status set to "error":

result = client.invoke("analytics-mcp", "analyze", token, params)

if result.status == "error":
    # The tool ran and returned an error payload. result.result holds it.
    logger.warning("Tool reported an error: %s", result.result)
const result = await client.invoke('analytics-mcp', 'analyze', token, params);

if (result.status === 'error') {
  console.warn('Tool reported an error:', result.result);
}

MCPInvocationResult carries mcp, action, result, status, execution_time / executionTime, verified, and metadata. See Data Models for the full shape.

How proxy responses map to exceptions

The proxy's HTTP status determines which exception you get:

StatusException
401, 403InvalidTokenException
409IntentMismatchException (with action and plan_hash populated)
anything else non-2xxMCPInvocationException (with status_code populated)

Policy decisions are separate: the proxy returns a structured enforcement response, and the SDK turns it into PolicyBlockedException or PolicyHoldException rather than mapping a status code.

Catching exceptions

Order matters. Catch specific classes before their bases, and remember TokenExpiredException is a subclass of InvalidTokenException.

import logging

from armoriq_sdk import (
    ArmorIQException,
    IntentMismatchException,
    InvalidTokenException,
    MCPInvocationException,
    PolicyBlockedException,
    PolicyHoldException,
    TokenExpiredException,
)

logger = logging.getLogger(__name__)

captured = client.capture_plan(llm="gpt-4", prompt="Analyze the data", plan=plan_dict)
token = client.get_intent_token(captured)

try:
    result = client.invoke("analytics-mcp", "analyze", token, params)

except TokenExpiredException as e:
    # Before InvalidTokenException — it is a subclass.
    logger.warning("Token %s expired at %s; re-minting", e.token_id, e.expired_at)
    token = client.get_intent_token(captured)
    result = client.invoke("analytics-mcp", "analyze", token, params)

except InvalidTokenException as e:
    # Signature rejected or token revoked. A fresh token needs a fresh plan.
    logger.error("Token %s rejected: %s", e.token_id, e)
    captured = client.capture_plan(llm="gpt-4", prompt="Analyze the data", plan=plan_dict)
    token = client.get_intent_token(captured)
    result = client.invoke("analytics-mcp", "analyze", token, params)

except IntentMismatchException as e:
    # The action was not in the plan this token covers.
    logger.error("Action %s not in plan %s", e.action, e.plan_hash)
    raise

except PolicyBlockedException as e:
    # A deny. Retrying will not change the answer.
    logger.error("Blocked by policy (%s): %s", e.enforcement_action, e.reason)
    raise

except PolicyHoldException as e:
    # Needs human approval. See the holds section below.
    logger.info("Held for approval: %s", e.delegation_context)
    raise

except MCPInvocationException as e:
    # Often the MCP itself, not ArmorIQ.
    logger.error("MCP %s.%s failed (HTTP %s): %s", e.mcp, e.action, e.status_code, e)
    raise

except ArmorIQException:
    # Anything else from the SDK.
    logger.exception("ArmorIQ error")
    raise
import {
  ArmorIQException,
  IntentMismatchException,
  InvalidTokenException,
  MCPInvocationException,
  PolicyBlockedException,
  PolicyHoldException,
  TokenExpiredException,
} from '@armoriq/sdk';

let captured = client.capturePlan('gpt-4', 'Analyze the data', planDict);
let token = await client.getIntentToken(captured);

try {
  const result = await client.invoke('analytics-mcp', 'analyze', token, params);
} catch (error) {
  if (error instanceof TokenExpiredException) {
    // Before InvalidTokenException — it is a subclass.
    console.warn(`Token ${error.tokenId} expired at ${error.expiredAt}; re-minting`);
    token = await client.getIntentToken(captured);
    await client.invoke('analytics-mcp', 'analyze', token, params);

  } else if (error instanceof InvalidTokenException) {
    console.error(`Token ${error.tokenId} rejected: ${error.message}`);
    captured = client.capturePlan('gpt-4', 'Analyze the data', planDict);
    token = await client.getIntentToken(captured);
    await client.invoke('analytics-mcp', 'analyze', token, params);

  } else if (error instanceof IntentMismatchException) {
    console.error(`Action ${error.action} not in plan ${error.planHash}`);
    throw error;

  } else if (error instanceof PolicyBlockedException) {
    console.error(`Blocked by policy (${error.enforcementAction}): ${error.reason}`);
    throw error;

  } else if (error instanceof PolicyHoldException) {
    console.info('Held for approval:', error.delegationContext);
    throw error;

  } else if (error instanceof MCPInvocationException) {
    console.error(`MCP ${error.mcp}.${error.action} failed (HTTP ${error.statusCode})`);
    throw error;

  } else if (error instanceof ArmorIQException) {
    console.error(`ArmorIQ error: ${error.message}`);
    throw error;

  } else {
    throw error;
  }
}

capture_plan and capturePlan raise a plain ValueError / Error, not an ArmorIQException, when the plan is missing or has no steps key. A bare except ArmorIQException will not catch it.

Holds and approvals

PolicyHoldException means the call is waiting on a human, not that it failed. Catching it and giving up turns every approval into an error.

Two ways to handle it:

  • invoke_with_policy / invokeWithPolicy can poll for the decision for you. See the TypeScript SDK page for the options.
  • In session mode, await_approval waits without blocking, so your app can stream held to approved live. See the Python SDK page for session setup.

The framework adapters do this already: on a hold they emit a hold event, wait for the decision, and let the tool run only on an explicit approve. See Strands for the event lifecycle.

RevokedTokenError is not raised yet

RevokedTokenError is exported from both SDKs and documented in their source as the signal that the IAP revoked a token mid-flight (the proxy answers 401 with X-Armoriq-Revoked: true).

No code path currently raises it. A 401 maps to InvalidTokenException regardless of that header, so a revoked token surfaces as a generic invalid token today.

Catch InvalidTokenException for now. It covers revocation along with signature failures, and the recovery is the same either way: capture a fresh plan and mint a new token.

On this page