Error Handling and Debugging | XRPL Development 101 | XRP Academy - XRP Academy
3 free lessons remaining this month

Free preview access resets monthly

Upgrade for Unlimited
Skip to main content
intermediate50 min

Error Handling and Debugging

Learning Objectives

Interpret transaction result codes and respond appropriately

Handle connection and network errors with retry logic

Debug failed transactions using ledger data

Implement idempotent operations for safe retries

Build comprehensive error handling for production applications

In development, you might see mostly successful transactions. In production:

  • Users submit transactions with insufficient balance
  • Network connections drop mid-transaction
  • Rate limits trigger during high-traffic periods
  • Sequence numbers collide with concurrent requests
  • Ledgers close before transactions are validated

The question isn't whether errors will occur, but whether your application handles them gracefully.


// Transaction results follow a naming convention

const resultCategories = {
// tes - Transaction Executed Successfully
tesSUCCESS: 'Transaction succeeded',

// tec - Transaction Executed, Claimed fee only
// Transaction was included but didn't achieve its goal
tecNO_DST: 'Destination account does not exist',
tecUNFUNDED_PAYMENT: 'Insufficient balance',
tecNO_LINE: 'No trust line exists',
tecPATH_PARTIAL: 'Payment couldn't deliver full amount',
tecOFFER_CROSSING: 'Offer would cross your own',
tecINSUFF_RESERVE_LINE: 'Would go below reserve',

// tef - Transaction Error - Failed before execution
// Transaction not executed, no fee charged
tefPAST_SEQ: 'Sequence number already used',
tefMAX_LEDGER: 'LastLedgerSequence already passed',
tefALREADY: 'Transaction already in ledger',

// tel - Transaction Error - Local
// Transaction rejected locally before submission
telINSUF_FEE_P: 'Fee too low for current network load',
telNO_DST_PARTIAL: 'Partial payment to nonexistent account',

// tem - Transaction Error - Malformed
// Transaction format is invalid
temMALFORMED: 'Transaction is malformed',
temBAD_AMOUNT: 'Invalid amount',
temBAD_CURRENCY: 'Invalid currency code',

// ter - Transaction Error - Retry
// Transaction not executed, may succeed if retried
terQUEUED: 'Transaction queued for later',
terPRE_SEQ: 'Earlier sequence number missing',
terRETRY: 'General retry'
};
```

// src/errors/result-handler.js

function handleTransactionResult(result) {
const code = result.result?.meta?.TransactionResult ||
result.result?.engine_result;

// Category 1: Success
if (code === 'tesSUCCESS') {
return { action: 'complete', retry: false };
}

// Category 2: Claimed fee but failed (tec)
// Transaction executed, won't change with retry
if (code.startsWith('tec')) {
return {
action: 'handle_failure',
retry: false,
reason: getTecReason(code)
};
}

// Category 3: Failed before execution (tef)
// May succeed if retried with different parameters
if (code.startsWith('tef')) {
if (code === 'tefPAST_SEQ') {
return { action: 'update_sequence', retry: true };
}
if (code === 'tefMAX_LEDGER') {
return { action: 'update_ledger_seq', retry: true };
}
return { action: 'investigate', retry: false };
}

// Category 4: Local rejection (tel)
if (code.startsWith('tel')) {
if (code === 'telINSUF_FEE_P') {
return { action: 'increase_fee', retry: true };
}
return { action: 'fix_transaction', retry: false };
}

// Category 5: Malformed (tem)
// Never retry - fix the transaction construction
if (code.startsWith('tem')) {
return { action: 'fix_code', retry: false };
}

// Category 6: Retry suggested (ter)
if (code.startsWith('ter')) {
return { action: 'retry_later', retry: true };
}

return { action: 'unknown', retry: false };
}

function getTecReason(code) {
const reasons = {
tecUNFUNDED_PAYMENT: 'Sender has insufficient XRP balance',
tecNO_DST: 'Destination account does not exist (and not enough XRP to create it)',
tecNO_LINE: 'Recipient has no trust line for this currency',
tecPATH_PARTIAL: 'Could not deliver the full amount (check SendMax)',
tecNO_DST_INSUF_XRP: 'Destination doesn't exist and payment too small to create',
tecINSUFF_RESERVE_LINE: 'Would drop below reserve',
tecOVERSIZE: 'Transaction too large',
tecFROZEN: 'Trust line is frozen',
tecNO_PERMISSION: 'Account doesn't allow this operation'
};
return reasons[code] || Transaction failed: ${code};
}

module.exports = { handleTransactionResult };
```

// src/errors/common-scenarios.js

class ErrorScenarios {

// Scenario: Insufficient balance
static async handleInsufficientBalance(account, requiredAmount) {
const balance = await getBalance(account);
const shortfall = requiredAmount - balance;

return {
error: 'INSUFFICIENT_BALANCE',
currentBalance: balance,
required: requiredAmount,
shortfall: shortfall,
suggestion: Need ${shortfall} more XRP to complete this transaction
};
}

// Scenario: No trust line
static async handleNoTrustLine(destination, currency, issuer) {
return {
error: 'NO_TRUST_LINE',
destination: destination,
currency: currency,
issuer: issuer,
suggestion: 'Recipient needs to create a trust line before receiving this token'
};
}

// Scenario: Sequence number mismatch
static async handleSequenceMismatch(account, usedSequence) {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();

const info = await client.request({
command: 'account_info',
account: account
});

const currentSeq = info.result.account_data.Sequence;

await client.disconnect();

return {
error: 'SEQUENCE_MISMATCH',
usedSequence: usedSequence,
currentSequence: currentSeq,
suggestion: Update sequence to ${currentSeq} and retry
};
}

// Scenario: Transaction expired
static handleTransactionExpired(lastLedgerSequence, currentLedger) {
return {
error: 'TRANSACTION_EXPIRED',
lastLedgerSequence: lastLedgerSequence,
currentLedger: currentLedger,
suggestion: 'Transaction expired before validation. Resubmit with new LastLedgerSequence'
};
}
}

module.exports = { ErrorScenarios };
```


// src/errors/retry.js

class RetryStrategy {
constructor(options = {}) {
this.maxAttempts = options.maxAttempts || 5;
this.baseDelayMs = options.baseDelayMs || 1000;
this.maxDelayMs = options.maxDelayMs || 30000;
this.jitterFactor = options.jitterFactor || 0.3;
}

calculateDelay(attempt) {
// Exponential backoff: 1s, 2s, 4s, 8s, 16s...
const exponentialDelay = this.baseDelayMs * Math.pow(2, attempt - 1);

// Cap at maximum
const cappedDelay = Math.min(exponentialDelay, this.maxDelayMs);

// Add jitter to prevent thundering herd
const jitter = cappedDelay * this.jitterFactor * (Math.random() * 2 - 1);

return Math.max(0, cappedDelay + jitter);
}

async execute(operation, isRetryable = () => true) {
let lastError;

for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
try {
return await operation(attempt);
} catch (error) {
lastError = error;

if (!isRetryable(error) || attempt === this.maxAttempts) {
throw error;
}

const delay = this.calculateDelay(attempt);
console.log(Attempt ${attempt} failed: ${error.message}. Retrying in ${Math.round(delay)}ms);

await this.sleep(delay);
}
}

throw lastError;
}

sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

// XRPL-specific retry logic
class XRPLRetry extends RetryStrategy {
isRetryableError(error) {
const code = error.data?.engine_result || error.message;

// Retry on network issues
if (error.message?.includes('timeout')) return true;
if (error.message?.includes('disconnected')) return true;
if (error.message?.includes('ECONNRESET')) return true;

// Retry on specific XRPL errors
const retryableCodes = [
'terQUEUED', 'terRETRY', 'terPRE_SEQ',
'telINSUF_FEE_P', 'tefPAST_SEQ', 'tefMAX_LEDGER'
];

return retryableCodes.some(c => code?.includes(c));
}

async executeXRPL(operation) {
return this.execute(operation, (error) => this.isRetryableError(error));
}
}

module.exports = { RetryStrategy, XRPLRetry };
```

// src/errors/submit-with-retry.js
const xrpl = require('xrpl');

class RobustTransactionSubmitter {
constructor(client, options = {}) {
this.client = client;
this.retry = new XRPLRetry(options);
this.sequenceCache = new Map();
}

async submitTransaction(wallet, transaction) {
return this.retry.executeXRPL(async (attempt) => {
console.log(Submission attempt ${attempt});

// Fresh autofill on each attempt
const prepared = await this.prepareTransaction(wallet, transaction);
const signed = wallet.sign(prepared);

// Submit
const submitResult = await this.client.submit(signed.tx_blob);

// Handle immediate rejection
const engineResult = submitResult.result.engine_result;
if (engineResult.startsWith('tem') || engineResult.startsWith('tef')) {
const error = new Error(Transaction rejected: ${engineResult});
error.data = submitResult.result;

// Update sequence cache on sequence errors
if (engineResult === 'tefPAST_SEQ') {
await this.refreshSequence(wallet.address);
}

throw error;
}

// Wait for validation
const hash = submitResult.result.tx_json.hash;
const validated = await this.waitForValidation(hash, prepared.LastLedgerSequence);

return validated;
});
}

async prepareTransaction(wallet, transaction) {
// Get fresh sequence
const sequence = await this.getSequence(wallet.address);

// Get current ledger for LastLedgerSequence
const serverInfo = await this.client.request({ command: 'server_info' });
const currentLedger = serverInfo.result.info.validated_ledger.seq;

// Calculate appropriate fee
const fee = await this.calculateFee();

return {
...transaction,
Account: wallet.address,
Sequence: sequence,
Fee: fee.toString(),
LastLedgerSequence: currentLedger + 20
};
}

async getSequence(address) {
// Check cache first
if (this.sequenceCache.has(address)) {
const cached = this.sequenceCache.get(address);
this.sequenceCache.set(address, cached + 1);
return cached;
}

await this.refreshSequence(address);
const seq = this.sequenceCache.get(address);
this.sequenceCache.set(address, seq + 1);
return seq;
}

async refreshSequence(address) {
const info = await this.client.request({
command: 'account_info',
account: address
});
this.sequenceCache.set(address, info.result.account_data.Sequence);
}

async calculateFee() {
const serverInfo = await this.client.request({ command: 'server_info' });
const loadFactor = serverInfo.result.info.load_factor || 1;
const baseFee = Number(serverInfo.result.info.validated_ledger.base_fee_xrp) * 1_000_000;

// Apply load factor with minimum
return Math.max(10, Math.ceil(baseFee * loadFactor));
}

async waitForValidation(hash, maxLedger) {
return new Promise((resolve, reject) => {
const checkInterval = setInterval(async () => {
try {
// Check transaction status
const txResult = await this.client.request({
command: 'tx',
transaction: hash
});

if (txResult.result.validated) {
clearInterval(checkInterval);
resolve(txResult.result);
}
} catch (error) {
// Transaction not found yet - keep waiting
}

// Check if we've passed max ledger
const serverInfo = await this.client.request({ command: 'server_info' });
const currentLedger = serverInfo.result.info.validated_ledger.seq;

if (currentLedger > maxLedger) {
clearInterval(checkInterval);
reject(new Error('Transaction expired - not validated in time'));
}
}, 1000);
});
}
}

module.exports = { RobustTransactionSubmitter };
```


// src/errors/debug.js
const xrpl = require('xrpl');

class TransactionDebugger {
constructor(client) {
this.client = client;
}

async investigateFailure(hash) {
console.log(\n=== Investigating Transaction ${hash} ===\n);

// Get transaction details
let tx;
try {
const response = await this.client.request({
command: 'tx',
transaction: hash,
binary: false
});
tx = response.result;
} catch (error) {
console.log('Transaction not found in ledger');
return { found: false };
}

console.log('Transaction Type:', tx.TransactionType);
console.log('Result:', tx.meta?.TransactionResult || 'Unknown');
console.log('Validated:', tx.validated);
console.log('Ledger Index:', tx.ledger_index);

// Analyze based on result
const result = tx.meta?.TransactionResult;

if (result === 'tesSUCCESS') {
console.log('\n✓ Transaction succeeded');
return this.analyzeSuccess(tx);
}

console.log('\n✗ Transaction failed');
return this.analyzeFailure(tx);
}

analyzeSuccess(tx) {
const analysis = {
success: true,
type: tx.TransactionType,
fee: Number(tx.Fee) / 1_000_000
};

// Parse delivered amount for payments
if (tx.TransactionType === 'Payment') {
const delivered = tx.meta.delivered_amount;
analysis.delivered = this.formatAmount(delivered);

// Check if partial
const requested = tx.Amount;
if (typeof delivered === 'string' && typeof requested === 'string') {
if (Number(delivered) < Number(requested)) {
analysis.partial = true;
analysis.requestedAmount = this.formatAmount(requested);
}
}
}

return analysis;
}

analyzeFailure(tx) {
const result = tx.meta.TransactionResult;
const analysis = {
success: false,
resultCode: result,
type: tx.TransactionType,
fee: Number(tx.Fee) / 1_000_000,
feeCharged: result.startsWith('tec') // tec claims fee
};

// Specific failure analysis
switch (result) {
case 'tecUNFUNDED_PAYMENT':
analysis.reason = 'Insufficient XRP balance';
analysis.suggestion = 'Check sender balance including reserve';
break;

case 'tecNO_DST':
analysis.reason = 'Destination account does not exist';
analysis.suggestion = 'Send at least 10 XRP to create account, or verify address';
break;

case 'tecNO_LINE':
analysis.reason = 'No trust line for currency';
analysis.suggestion = 'Recipient must create trust line first';
break;

case 'tecPATH_PARTIAL':
analysis.reason = 'Could not deliver full amount';
analysis.suggestion = 'Check liquidity, increase SendMax, or reduce amount';
break;

case 'tecINSUFF_RESERVE_LINE':
analysis.reason = 'Would go below reserve';
analysis.suggestion = 'Account needs more XRP for reserve';
break;

default:
analysis.reason = Failed with code: ${result};
analysis.suggestion = 'Check XRPL documentation for this error code';
}

console.log('\nFailure Analysis:');
console.log(' Reason:', analysis.reason);
console.log(' Suggestion:', analysis.suggestion);
console.log(' Fee charged:', analysis.feeCharged ? 'Yes' : 'No');

return analysis;
}

formatAmount(amount) {
if (typeof amount === 'string') {
return { currency: 'XRP', value: Number(amount) / 1_000_000 };
}
return {
currency: amount.currency,
issuer: amount.issuer,
value: Number(amount.value)
};
}

// Pre-submission validation
async validateBeforeSubmit(transaction, wallet) {
const issues = [];

// Check account exists and has balance
try {
const info = await this.client.request({
command: 'account_info',
account: wallet.address
});

const balance = Number(info.result.account_data.Balance);
const ownerCount = info.result.account_data.OwnerCount;
const reserve = 10_000_000 + (ownerCount * 2_000_000);
const available = balance - reserve;

// Check fee
const fee = Number(transaction.Fee || 12);
if (fee > available) {
issues.push(Insufficient balance for fee. Available: ${available / 1_000_000} XRP);
}

// Check payment amount
if (transaction.TransactionType === 'Payment') {
const amount = typeof transaction.Amount === 'string'
? Number(transaction.Amount)
: 0;

if (amount > available - fee) {
issues.push(Insufficient balance for payment. Available: ${(available - fee) / 1_000_000} XRP);
}
}

} catch (error) {
if (error.data?.error === 'actNotFound') {
issues.push('Sender account does not exist');
} else {
issues.push(Could not verify account: ${error.message});
}
}

// Check destination for payments
if (transaction.TransactionType === 'Payment') {
try {
await this.client.request({
command: 'account_info',
account: transaction.Destination
});
} catch (error) {
if (error.data?.error === 'actNotFound') {
const amount = typeof transaction.Amount === 'string'
? Number(transaction.Amount)
: 0;

if (amount < 10_000_000) {
issues.push('Destination does not exist and amount is less than 10 XRP (account reserve)');
}
}
}
}

return {
valid: issues.length === 0,
issues: issues
};
}
}

module.exports = { TransactionDebugger };
```

// src/errors/logging.js

class XRPLLogger {
constructor(options = {}) {
this.level = options.level || 'info';
this.includeTimestamp = options.includeTimestamp !== false;
}

log(level, message, data = {}) {
const entry = {
timestamp: this.includeTimestamp ? new Date().toISOString() : undefined,
level,
message,
...data
};

// In production, send to logging service
console.log(JSON.stringify(entry));
}

// Transaction lifecycle logging
logTransactionStart(txType, params) {
this.log('info', 'Transaction started', {
event: 'tx_start',
type: txType,
params: this.sanitize(params)
});
}

logTransactionSubmit(hash, result) {
this.log('info', 'Transaction submitted', {
event: 'tx_submit',
hash,
engineResult: result
});
}

logTransactionComplete(hash, result, duration) {
this.log('info', 'Transaction complete', {
event: 'tx_complete',
hash,
result,
durationMs: duration
});
}

logTransactionError(hash, error, context) {
this.log('error', 'Transaction failed', {
event: 'tx_error',
hash,
error: error.message,
code: error.data?.engine_result,
context: this.sanitize(context)
});
}

// Connection logging
logConnection(event, details) {
this.log('info', Connection ${event}, {
event: connection_${event},
...details
});
}

// Sanitize sensitive data
sanitize(data) {
const sanitized = { ...data };

// Remove sensitive fields
const sensitiveFields = ['seed', 'secret', 'privateKey', 'mnemonic'];
for (const field of sensitiveFields) {
if (sanitized[field]) {
sanitized[field] = '[REDACTED]';
}
}

return sanitized;
}
}

// Usage
const logger = new XRPLLogger();

async function submitWithLogging(wallet, transaction) {
const startTime = Date.now();
logger.logTransactionStart(transaction.TransactionType, transaction);

try {
const prepared = await client.autofill(transaction);
const signed = wallet.sign(prepared);
const result = await client.submitAndWait(signed.tx_blob);

logger.logTransactionComplete(
signed.hash,
result.result.meta.TransactionResult,
Date.now() - startTime
);

return result;
} catch (error) {
logger.logTransactionError(null, error, { transaction });
throw error;
}
}

module.exports = { XRPLLogger };
```


// The problem: Network timeout during submission
// Did the transaction execute? We don't know.
// If we retry and it did execute, we might double-pay.

// Solution: Design operations to be safely retryable

class IdempotentPayment {
constructor(client) {
this.client = client;
this.processedIds = new Set(); // In production, use persistent storage
}

async sendPayment(paymentId, sender, destination, amount) {
// Check if already processed
if (this.processedIds.has(paymentId)) {
console.log(Payment ${paymentId} already processed);
return { duplicate: true };
}

// Check if transaction already on ledger (by memo)
const existing = await this.findExistingPayment(paymentId, sender);
if (existing) {
this.processedIds.add(paymentId);
return { duplicate: true, hash: existing.hash };
}

// Include paymentId in memos for idempotency tracking
const payment = {
TransactionType: 'Payment',
Account: sender.address,
Destination: destination,
Amount: amount,
Memos: [{
Memo: {
MemoType: Buffer.from('payment_id').toString('hex'),
MemoData: Buffer.from(paymentId).toString('hex')
}
}]
};

const result = await this.submitWithRetry(sender, payment);
this.processedIds.add(paymentId);

return result;
}

async findExistingPayment(paymentId, sender) {
// Search recent transactions for this payment ID
const history = await this.client.request({
command: 'account_tx',
account: sender.address,
limit: 50
});

for (const tx of history.result.transactions) {
if (tx.tx.Memos) {
for (const memo of tx.tx.Memos) {
const memoData = Buffer.from(memo.Memo.MemoData || '', 'hex').toString();
if (memoData === paymentId) {
return tx.tx;
}
}
}
}

return null;
}

async submitWithRetry(wallet, transaction) {
// ... retry logic
}
}
```

// src/errors/sequence-manager.js

class SequenceManager {
constructor(client) {
this.client = client;
this.sequences = new Map(); // address -> { next, pending }
this.locks = new Map(); // address -> Promise
}

async acquireSequence(address) {
// Wait for any pending operations on this account
while (this.locks.has(address)) {
await this.locks.get(address);
}

// Create lock
let releaseLock;
this.locks.set(address, new Promise(resolve => { releaseLock = resolve; }));

try {
// Get or refresh sequence
if (!this.sequences.has(address)) {
await this.refreshSequence(address);
}

const state = this.sequences.get(address);
const sequence = state.next;
state.next++;
state.pending.add(sequence);

return {
sequence,
release: () => this.releaseSequence(address, sequence),
fail: () => this.failSequence(address, sequence)
};
} finally {
this.locks.delete(address);
releaseLock();
}
}

async refreshSequence(address) {
const info = await this.client.request({
command: 'account_info',
account: address
});

this.sequences.set(address, {
next: info.result.account_data.Sequence,
pending: new Set()
});
}

releaseSequence(address, sequence) {
const state = this.sequences.get(address);
if (state) {
state.pending.delete(sequence);
}
}

failSequence(address, sequence) {
// On failure, refresh to ensure we're in sync
this.sequences.delete(address);
}
}

// Usage
async function sendPaymentWithSequenceManager(manager, wallet, payment) {
const seqHandle = await manager.acquireSequence(wallet.address);

try {
const tx = { ...payment, Sequence: seqHandle.sequence };
const result = await submitTransaction(wallet, tx);
seqHandle.release();
return result;
} catch (error) {
seqHandle.fail();
throw error;
}
}
```


// src/errors/index.js - Complete error handling module

const xrpl = require('xrpl');

class XRPLErrorHandler {
constructor(client, options = {}) {
this.client = client;
this.logger = options.logger || console;
this.retry = new XRPLRetry(options.retry);
this.sequenceManager = new SequenceManager(client);
this.debugger = new TransactionDebugger(client);
}

// Main entry point for safe transaction submission
async submitSafely(wallet, transaction, options = {}) {
const startTime = Date.now();

try {
// Pre-validation
if (options.validate !== false) {
const validation = await this.debugger.validateBeforeSubmit(transaction, wallet);
if (!validation.valid) {
throw new XRPLValidationError(validation.issues);
}
}

// Acquire sequence
const seqHandle = await this.sequenceManager.acquireSequence(wallet.address);

try {
// Prepare transaction
const prepared = await this.prepareTransaction(wallet, transaction, seqHandle.sequence);

// Sign
const signed = wallet.sign(prepared);

// Submit with retry
const result = await this.submitWithRetry(signed.tx_blob, prepared.LastLedgerSequence);

// Success
seqHandle.release();

this.logger.log('info', 'Transaction successful', {
hash: signed.hash,
result: result.meta.TransactionResult,
duration: Date.now() - startTime
});

return {
success: true,
hash: signed.hash,
result: result
};

} catch (error) {
seqHandle.fail();
throw error;
}

} catch (error) {
this.logger.log('error', 'Transaction failed', {
error: error.message,
type: error.constructor.name,
duration: Date.now() - startTime
});

return {
success: false,
error: this.formatError(error)
};
}
}

async prepareTransaction(wallet, transaction, sequence) {
const serverInfo = await this.client.request({ command: 'server_info' });
const info = serverInfo.result.info;

return {
...transaction,
Account: wallet.address,
Sequence: sequence,
Fee: this.calculateFee(info).toString(),
LastLedgerSequence: info.validated_ledger.seq + 20
};
}

calculateFee(serverInfo) {
const baseFee = Number(serverInfo.validated_ledger.base_fee_xrp) * 1_000_000;
const loadFactor = serverInfo.load_factor || 1;
return Math.max(10, Math.ceil(baseFee * loadFactor * 1.2)); // 20% buffer
}

async submitWithRetry(txBlob, maxLedger) {
return this.retry.executeXRPL(async () => {
const submitResult = await this.client.submit(txBlob);
const engineResult = submitResult.result.engine_result;

// Handle immediate failures
if (this.isImmediateFailure(engineResult)) {
const error = new XRPLTransactionError(engineResult, submitResult.result);
throw error;
}

// Wait for validation
return this.waitForValidation(submitResult.result.tx_json.hash, maxLedger);
});
}

isImmediateFailure(result) {
return result.startsWith('tem') ||
(result.startsWith('tef') && result !== 'tefPAST_SEQ' && result !== 'tefMAX_LEDGER');
}

async waitForValidation(hash, maxLedger) {
const startTime = Date.now();
const timeout = 60000; // 1 minute max

while (Date.now() - startTime < timeout) {
try {
const result = await this.client.request({
command: 'tx',
transaction: hash
});

if (result.result.validated) {
return result.result;
}
} catch (e) {
// Not found yet
}

// Check if expired
const serverInfo = await this.client.request({ command: 'server_info' });
if (serverInfo.result.info.validated_ledger.seq > maxLedger) {
throw new XRPLTransactionExpiredError(hash, maxLedger);
}

await new Promise(r => setTimeout(r, 1000));
}

throw new XRPLTransactionTimeoutError(hash);
}

formatError(error) {
return {
type: error.constructor.name,
message: error.message,
code: error.code,
retryable: error.retryable || false,
suggestion: error.suggestion
};
}
}

// Custom error classes
class XRPLValidationError extends Error {
constructor(issues) {
super(Validation failed: ${issues.join(', ')});
this.name = 'XRPLValidationError';
this.issues = issues;
this.retryable = false;
}
}

class XRPLTransactionError extends Error {
constructor(code, result) {
super(Transaction error: ${code});
this.name = 'XRPLTransactionError';
this.code = code;
this.result = result;
this.retryable = ['terQUEUED', 'terRETRY', 'terPRE_SEQ'].includes(code);
}
}

class XRPLTransactionExpiredError extends Error {
constructor(hash, maxLedger) {
super(Transaction ${hash} expired at ledger ${maxLedger});
this.name = 'XRPLTransactionExpiredError';
this.hash = hash;
this.retryable = true;
}
}

class XRPLTransactionTimeoutError extends Error {
constructor(hash) {
super(Timeout waiting for transaction ${hash});
this.name = 'XRPLTransactionTimeoutError';
this.hash = hash;
this.retryable = true;
}
}

module.exports = {
XRPLErrorHandler,
XRPLValidationError,
XRPLTransactionError,
XRPLTransactionExpiredError,
XRPLTransactionTimeoutError
};
```


Error handling separates amateur code from production code. XRPL provides clear error codes, but you must handle them appropriately. Invest in robust error handling early—it's far harder to retrofit than to build correctly from the start.


Assignment: Build a comprehensive error handling module for XRPL applications.

Requirements:

  • Parse and categorize all result codes

  • Determine retry appropriateness

  • Generate user-friendly error messages

  • Implement exponential backoff with jitter

  • Handle XRPL-specific retry scenarios

  • Track retry attempts and outcomes

  • Investigate failed transactions

  • Pre-validate before submission

  • Generate actionable error reports

  • Concurrent-safe sequence allocation

  • Recovery from sequence conflicts

  • Batch transaction support

  • Error classification comprehensive (25%)

  • Retry logic robust (25%)

  • Debugging tools useful (25%)

  • Sequence management handles edge cases (25%)

Time investment: 3-4 hours
Value: Production-ready error handling for any XRPL application


Knowledge Check

Question 1 of 2

Your transaction fails with `telINSUF_FEE_P`. What does this mean and what should you do?

For Next Lesson:
You now understand error handling. Lesson 15 covers security best practices—protecting keys, preventing common vulnerabilities, and building secure applications.


End of Lesson 14

Total words: ~5,400
Estimated completion time: 50 minutes reading + 3-4 hours for deliverable

Key Takeaways

1

Result codes tell you what happened

: Learn the prefix meanings (tes, tec, tef, tel, tem, ter) and handle each appropriately.

2

Retry only what's retryable

: 'tem' errors won't succeed with retry; 'ter' errors might. Know the difference.

3

Pre-validate when possible

: Catch errors before incurring fees by checking balances, destinations, and trust lines first.

4

Design for idempotency

: Network failures will cause uncertainty. Make operations safely retryable.

5

Log everything

: When things go wrong in production, logs are your only debugging tool. ---