Skip to main content
Version: 0.96.0

Error Handling

Error Types

Core Errors

ErrorDescription
CCIPTransactionNotFoundErrorTransaction hash doesn't exist
CCIPMessageNotFoundInTxErrorTransaction contains no CCIP messages
CCIPMessageIdNotFoundErrorMessage ID not found after searching
CCIPMessageDecodeErrorFailed to decode message data
CCIPBlockNotFoundErrorBlock doesn't exist or isn't finalized
CCIPOffRampNotFoundErrorNo OffRamp found for the lane
CCIPMerkleRootMismatchErrorMerkle proof validation failed

API Errors

ErrorDescription
CCIPHttpErrorHTTP request failed (includes status code)
CCIPApiClientNotAvailableErrorAPI disabled with apiClient: null
CCIPMessageRetrievalErrorBoth API and RPC failed to retrieve message
CCIPTimeoutErrorRequest timed out (transient, safe to retry)
CCIPUnexpectedPaginationErrorTransaction contains >100 CCIP messages
CCIPMessageIdValidationErrorInvalid message ID format

Basic Error Handling

TypeScript
import { EVMChain } from '@chainlink/ccip-sdk'

const chain = await EVMChain.fromUrl('https://rpc.sepolia.org')

try {
const requests = await chain.getMessagesInTx('0x1234...')
console.log('Found', requests.length, 'messages')
} catch (error) {
if (error.message.includes('not found')) {
console.log('Transaction not found - check the hash')
} else if (error.message.includes('no CCIP messages')) {
console.log('Transaction exists but has no CCIP messages')
} else {
console.error('Unexpected error:', error)
}
}

Search Failures

Handle search failures when looking up messages by ID:

TypeScript
import { EVMChain } from '@chainlink/ccip-sdk'

async function findMessage(messageId: string) {
const chain = await EVMChain.fromUrl('https://rpc.sepolia.org')

try {
const request = await chain.getMessageById(messageId)
return request
} catch (error) {
if (error.message.includes('not found')) {
console.log('Message not found - it may be:')
console.log(' - Very old (before search window)')
console.log(' - On a different chain')
console.log(' - Invalid message ID')
return null
}
throw error
}
}

The getMessageById method uses the CCIP API to look up messages by their unique ID.

Execution States

Messages can fail execution for several reasons:

TypeScript
import { EVMChain, ExecutionState } from '@chainlink/ccip-sdk'

async function checkExecutionStatus(dest: EVMChain, offRamp: string, request: any) {
for await (const execution of dest.getExecutionReceipts({
offRamp,
messageId: request.message.messageId,
})) {
switch (execution.receipt.state) {
case ExecutionState.Success:
console.log('Message executed successfully')
return 'success'

case ExecutionState.Failed:
console.log('Execution failed - receiver reverted')
console.log('Return data:', execution.receipt.returnData)
return 'failed'

case ExecutionState.InProgress:
console.log('Message execution in progress')
return 'pending'
}
}
return 'not_found'
}

Manual Execution

When automatic execution fails, manually execute the message.

Step 1: Gather Data

TypeScript
import {
EVMChain,
discoverOffRamp
} from '@chainlink/ccip-sdk'

const source = await EVMChain.fromUrl('https://rpc.sepolia.org')
const dest = await EVMChain.fromUrl('https://rpc.fuji.avax.network')

const requests = await source.getMessagesInTx('0x1234...')
const request = requests[0]

const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp)

const commit = await dest.getCommitReport({
commitStore: offRamp,
request,
})

if (!commit) {
throw new Error('Message not yet committed - cannot execute')
}

Step 2: Calculate Merkle Proof

TypeScript
import { calculateManualExecProof } from '@chainlink/ccip-sdk'

// Fetch all messages in the commit batch
const messagesInBatch = await source.getMessagesInBatch(request, commit.report)

const proof = calculateManualExecProof(
messagesInBatch,
request.lane,
request.message.messageId,
commit.report.merkleRoot
)

console.log('Merkle root:', proof.merkleRoot)
console.log('Proof hashes:', proof.proofs)

Step 3: Execute

TypeScript
const executionReport = {
...proof,
message: request.message,
offchainTokenData: [],
}

const execution = await dest.executeReport({
offRamp,
execReport: executionReport,
wallet, // Required: signer instance
})
console.log('Manual execution tx:', execution.log.transactionHash)

Merkle Root Mismatches

If the calculated merkle root doesn't match the commit:

TypeScript
import { calculateManualExecProof } from '@chainlink/ccip-sdk'

try {
const proof = calculateManualExecProof(
messagesInBatch,
request.lane,
request.message.messageId,
commit.report.merkleRoot
)
} catch (error) {
if (error.message.includes('Merkle root mismatch')) {
console.log('Merkle root mismatch - possible causes:')
console.log(' - Messages in batch are incomplete')
console.log(' - Wrong lane configuration')
console.log(' - Message was modified')

// Try without validation to see calculated root
const unvalidatedProof = calculateManualExecProof(
messagesInBatch,
request.lane,
request.message.messageId
)
console.log('Calculated root:', unvalidatedProof.merkleRoot)
console.log('Expected root:', commit.report.merkleRoot)
}
}

Retry Logic

Implement retry logic for transient failures:

TypeScript
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
delayMs = 1000
): Promise<T> {
let lastError: Error | undefined

for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn()
} catch (error) {
lastError = error as Error

// Don't retry on definitive failures
if (
error.message.includes('not found') ||
error.message.includes('invalid')
) {
throw error
}

console.log(`Attempt ${attempt} failed, retrying in ${delayMs}ms...`)
await new Promise(resolve => setTimeout(resolve, delayMs))
delayMs *= 2 // Exponential backoff
}
}

throw lastError
}

const request = await withRetry(() =>
chain.getMessageById(messageId)
)

Complete Recovery Example

TypeScript
import {
EVMChain,
calculateManualExecProof,
discoverOffRamp,
ExecutionState,
} from '@chainlink/ccip-sdk'

async function recoverFailedMessage(sourceTxHash: string) {
const source = await EVMChain.fromUrl('https://rpc.sepolia.org')
const dest = await EVMChain.fromUrl('https://rpc.fuji.avax.network')

// Step 1: Get the request
const requests = await source.getMessagesInTx(sourceTxHash)
const request = requests[0]
console.log('Message ID:', request.message.messageId)

// Step 2: Find OffRamp
const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp)

// Step 3: Check current execution status
let needsManualExecution = false
for await (const execution of dest.getExecutionReceipts({
offRamp,
messageId: request.message.messageId,
})) {
if (execution.receipt.state === ExecutionState.Success) {
console.log('Already executed')
return { status: 'already_executed' }
}
if (execution.receipt.state === ExecutionState.Failed) {
console.log('Previous execution failed, attempting manual execution...')
needsManualExecution = true
break
}
}

// Step 4: Get commit
const commit = await dest.getCommitReport({
commitStore: offRamp,
request,
})
if (!commit) {
console.log('Not yet committed - wait for DON to commit')
return { status: 'pending_commit' }
}

// Step 5: Manual execution
if (needsManualExecution) {
// Fetch all messages in the commit batch
const messagesInBatch = await source.getMessagesInBatch(request, commit.report)

const proof = calculateManualExecProof(
messagesInBatch,
request.lane,
request.message.messageId,
commit.report.merkleRoot
)

console.log('Executing manually...')
const execution = await dest.executeReport({
offRamp,
execReport: {
...proof,
message: request.message,
offchainTokenData: [],
},
wallet, // Required: signer instance
})

console.log('Manual execution tx:', execution.log.transactionHash)
return { status: 'manually_executed', tx: execution.log.transactionHash }
}

return { status: 'pending_execution' }
}

Error Parsing

The SDK provides chain-specific error parsing to decode CCIP contract errors into human-readable messages.

EVM Error Parsing

Parse errors from EVM transaction reverts:

TypeScript
import { EVMChain } from '@chainlink/ccip-sdk'

try {
await publicClient.call({ to, data, value })
} catch (error) {
const parsed = EVMChain.parse(error)
if (parsed) {
// parsed contains keys like 'revert', 'revert.ChainNotAllowed', etc.
console.log('Error:', parsed)
// => { revert: 'ChainNotAllowed(uint64 destChainSelector)', ... }

// Extract error name
for (const [key, value] of Object.entries(parsed)) {
if (key.startsWith('revert') && typeof value === 'string') {
const match = value.match(/^(\w+)\(/)
if (match) {
console.log('Error name:', match[1]) // e.g., 'ChainNotAllowed'
}
}
}
}
}

Solana Error Parsing

Parse errors from Solana transaction logs:

TypeScript
import { SolanaChain } from '@chainlink/ccip-sdk'

try {
await sendTransaction(transaction, connection)
} catch (error) {
// Pass the error (which may contain logs) or transaction logs directly
const parsed = SolanaChain.parse(error.logs || error)
if (parsed) {
console.log('Error:', parsed)
// => { program: '...', error: 'Rate limit exceeded', ... }
}
}

Common CCIP Errors

ErrorDescriptionSolution
ChainNotAllowedDestination chain not enabled for this tokenUse a supported route
RateLimitReachedToken bucket rate limit exceededTry smaller amount or wait for refill
UnsupportedTokenToken not supported on this laneUse a different token or route
InsufficientFeeTokenAmountNot enough fee providedEnsure sufficient native tokens
InvalidReceiverReceiver address format invalidCheck address format for destination chain
SenderNotAllowedSender not on allowlistContact token issuer for allowlist
InvalidExtraArgsTagInvalid extra args encodingUse encodeExtraArgs() helper
MessageTooLargeMessage data exceeds max sizeReduce data payload size
TokenMaxCapacityExceededTransfer exceeds pool capacityTry smaller amount

CCIPError Class

The SDK provides a base error class with useful properties:

TypeScript
import { CCIPError, getRetryDelay } from '@chainlink/ccip-sdk'

try {
const message = await chain.getMessageById(messageId)
} catch (error) {
if (CCIPError.isCCIPError(error)) {
console.log('Message:', error.message)
console.log('Is transient:', error.isTransient) // Can retry?
console.log('Retry after:', error.retryAfterMs) // Suggested delay
console.log('Recovery hint:', error.recovery)

// Use SDK utility for retry delay
const delay = getRetryDelay(error)
if (delay !== null) {
await new Promise(resolve => setTimeout(resolve, delay))
// Retry the operation...
}
}
}

Expected Errors During Polling

CCIPMessageIdNotFoundError is expected when polling for a recently sent message:

TypeScript
import { CCIPMessageIdNotFoundError } from '@chainlink/ccip-sdk'

try {
const message = await chain.getMessageById(messageId)
} catch (error) {
if (error instanceof CCIPMessageIdNotFoundError) {
// Expected - message not indexed yet, keep polling
console.log('Message not found yet, will retry...')
} else {
throw error
}
}

Retry Utility

The SDK exports a withRetry utility for implementing custom retry logic with exponential backoff:

TypeScript
import { withRetry, DEFAULT_API_RETRY_CONFIG } from '@chainlink/ccip-sdk'

const result = await withRetry(
async () => {
// Your async operation that may fail transiently
return await someApiCall()
},
{
maxRetries: 3, // Max retry attempts (default: 3)
initialDelayMs: 1000, // Initial delay before first retry (default: 1000)
backoffMultiplier: 2, // Multiplier for exponential backoff (default: 2)
maxDelayMs: 30000, // Maximum delay cap (default: 30000)
respectRetryAfterHint: true, // Use error's retryAfterMs when available
logger: console, // Optional: logs retry attempts
},
)

The utility only retries on transient errors (5xx HTTP errors, timeouts). Non-transient errors (4xx, validation errors) are thrown immediately.

Checking Transient Errors

TypeScript
import { isTransientError } from '@chainlink/ccip-sdk'

try {
const result = await chain.getMessageById(messageId)
} catch (error) {
if (isTransientError(error)) {
console.log('Transient error - safe to retry')
} else {
console.log('Permanent error - do not retry')
throw error
}
}

API Mode Configuration

By default, Chain instances use the CCIP API for enhanced functionality. You can configure this behavior:

TypeScript
import { EVMChain, DEFAULT_API_RETRY_CONFIG } from '@chainlink/ccip-sdk'

// Default: API enabled with automatic retry on fallback
const chain = await EVMChain.fromUrl(url)

// Custom retry configuration for API fallback operations
const chainWithRetry = await EVMChain.fromUrl(url, {
apiRetryConfig: {
maxRetries: 5,
initialDelayMs: 2000,
backoffMultiplier: 1.5,
maxDelayMs: 60000,
respectRetryAfterHint: true,
},
})

// Fully decentralized mode - uses only RPC data, no API
const decentralizedChain = await EVMChain.fromUrl(url, { apiClient: null })

Decentralized Mode

Disable the API entirely for fully decentralized operation:

TypeScript
// Opt-out of API - uses only RPC data
const chain = await EVMChain.fromUrl(url, { apiClient: null })

// API-dependent methods will throw CCIPApiClientNotAvailableError
await chain.getLaneLatency(destSelector) // Throws