Trivia Royale GuidePatterns & Examples
Error Handling
Common issues and how to handle them gracefully
Error Handling
Building robust state channel applications requires handling various failure modes. This guide covers common errors and proven recovery strategies.
Connection Errors
WebSocket Disconnection
const client = createBetterNitroliteClient({
wallet,
onAppMessage: (type, sessionId, data) => {
// ... handle messages
}
});
try {
await client.connect();
} catch (error) {
console.error('Connection failed:', error.message);
// Retry with exponential backoff
await retryWithBackoff(async () => await client.connect(), 3);
}
async function retryWithBackoff(fn: () => Promise<void>, maxRetries: number) {
for (let i = 0; i < maxRetries; i++) {
try {
await fn();
return; // Success
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
}
}
}
Fund Management Errors
Insufficient Wallet Balance
try {
await client.deposit(parseUSDC('1000'));
} catch (error) {
if (error.message.includes('Insufficient funds')) {
// Show user their actual balance
const balances = await client.getBalances();
console.error(`You have ${formatUSDC(balances.wallet)}, need ${formatUSDC(parseUSDC('1000'))}`);
// Suggest smaller amount
const affordable = balances.wallet;
console.log(`Try depositing ${formatUSDC(affordable)} instead`);
}
}
Insufficient Channel Capacity
try {
await client.send({ to: recipient, amount: parseUSDC('50') });
} catch (error) {
const balances = await client.getBalances();
const available = balances.channel + balances.ledger;
console.error(`Insufficient capacity: ${formatUSDC(available)} available, ${formatUSDC(parseUSDC('50'))} requested`);
console.log(`Deposit more funds or reduce payment amount`);
}
Session Errors
Signature Collection Timeout
async function collectSignatures(sessionId: string, timeout: number = 30000) {
return new Promise((resolve, reject) => {
const signatures: string[] = [];
const timer = setTimeout(() => {
reject(new Error('Timeout collecting signatures'));
}, timeout);
// ... collect signatures ...
if (signatures.length === expectedCount) {
clearTimeout(timer);
resolve(signatures);
}
});
}
try {
const signatures = await collectSignatures(gameId, 30000);
} catch (error) {
// Notify players
notifyPlayers(gameId, {
type: 'game_cancelled',
reason: 'Not all players signed in time'
});
// Clean up
await db.games.update(gameId, { status: 'CANCELLED' });
}
Session Already Closed
async function safeSendMessage(sessionId: Hex, type: string, data: any) {
const activeSessions = client.getActiveSessions();
if (!activeSessions.includes(sessionId)) {
throw new Error(`Session ${sessionId} is not active`);
}
try {
await client.sendMessage(sessionId, type, data);
} catch (error) {
if (error.message.includes('not active')) {
// Session closed during send
console.warn('Session closed, cleaning up');
cleanupSession(sessionId);
}
throw error;
}
}
Message Handling Errors
Invalid Message Data
onAppMessage: async (type, sessionId, data) => {
try {
if (type === 'answer') {
// Validate data structure
if (!data.from || !data.answer) {
console.error('Invalid answer format:', data);
return; // Ignore malformed message
}
processAnswer(data);
}
} catch (error) {
console.error('Error handling message:', error);
// Don't crash - log and continue
}
}
State Consistency Errors
Balance Mismatch After Operation
const balancesBefore = await client.getBalances();
const expectedChange = -parseUSDC('5');
await client.send({ to: recipient, amount: parseUSDC('5') });
// Wait for state to settle
await new Promise(r => setTimeout(r, 1000));
const balancesAfter = await client.getBalances();
const actualChange = balancesAfter.ledger - balancesBefore.ledger;
if (actualChange !== expectedChange) {
console.error('Balance mismatch!');
console.error(`Expected: ${formatUSDC(expectedChange)}, Actual: ${formatUSDC(actualChange)}`);
// Re-query to get consistent state
const freshBalances = await client.getBalances();
console.log('Fresh balances:', freshBalances);
}
Best Practices
1. Always Verify State After Operations
// Before
const before = await client.getBalances();
// Operation
await client.deposit(amount);
// After (with delay)
await new Promise(r => setTimeout(r, 1000));
const after = await client.getBalances();
// Verify
const expectedIncrease = amount;
const actualIncrease = after.channel - before.channel;
if (actualIncrease !== expectedIncrease) {
console.warn('Unexpected balance change');
}
2. Implement Timeouts Everywhere
async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
// Usage
try {
await withTimeout(client.connect(), 10000);
} catch (error) {
console.error('Connection timeout');
}
3. Handle Partial Failures
try {
// Complex operation with multiple steps
await client.deposit(parseUSDC('10')); // Step 1
await createSession(...); // Step 2
await sendMessage(...); // Step 3
} catch (error) {
// Check which step failed
const balances = await client.getBalances();
if (balances.channel > initialChannel) {
console.log('Deposit succeeded, but session creation failed');
// Decide: retry session creation or withdraw
} else {
console.log('Deposit failed');
// Retry from beginning
}
}
4. Graceful Degradation
async function sendWithFallback(to: Address, amount: bigint) {
try {
// Try instant off-chain transfer
await client.send({ to, amount });
} catch (error) {
console.warn('Off-chain transfer failed, falling back to on-chain');
// Fallback to standard ERC-20 transfer
const tx = await usdcContract.transfer(to, amount);
await tx.wait();
}
}
Next Steps
- Fund Management: Understanding balance operations
- Session Lifecycle: Managing session state
- Complete Game: See error handling in context