Skip to main content

Error Handling

The KEA Wallet SDK throws standard JavaScript Error objects with an added .code property for programmatic handling. Errors come from two sources: promise rejections when calling SDK methods like connect() or signTransaction(), and background events emitted by the SDK when something goes wrong asynchronously (e.g., iframe communication failure or wallet lock).

This guide covers every error type the SDK can produce, how to catch them, and patterns for graceful recovery.

Prerequisites

The Error Object

Every error thrown by the SDK is a standard Error with an ErrorCode attached via the code property:

import { ErrorCode } from '@kea-wallet/browser-sdk';

try {
await connect({ metadata: { appName: 'My App' } });
} catch (err) {
const error = err as Error & { code?: string };
switch (error.code) {
case ErrorCode.USER_REJECTED:
// User closed the modal or clicked Reject
break;
case ErrorCode.TIMEOUT:
// Operation timed out
break;
default:
console.error('Unexpected error:', error.code, error.message);
}
}
warning

Always use error.code for branching logic, never error.message. Error messages may change between SDK versions; error codes are stable.

Error Code Reference

The SDK defines 11 error codes in the ErrorCode constant, re-exported from all SDK packages (@kea-wallet/browser-sdk, @kea-wallet/react-sdk):

CodeDescriptionWhen it occursRecommended UI action
USER_REJECTEDUser clicked Reject or closed the modalconnect(), signTransaction(), signMessage()Dismiss silently or show a brief toast
WALLET_LOCKEDWallet is currently lockedAny operation while the wallet is lockedShow "Please unlock your wallet" prompt
INVALID_PASSWORDWrong password enteredconnect() with password authenticationShow "Invalid password" with retry option
ALREADY_CONNECTEDDuplicate connection attemptconnect() when already connectedNo-op — use the existing connection
ACCOUNT_NOT_FOUNDUnknown public keyselectAccount() with an invalid addressShow "Account not found"
INVALID_TRANSACTIONMalformed transaction payloadsignTransaction()Show "Invalid transaction" — this is a developer error
TRANSACTION_FAILEDTransaction execution failed on-chainsignTransaction()Show failure reason, suggest retry
INSUFFICIENT_FUNDSNot enough balancesignTransaction()Show balance and required amount
NETWORK_ERRORNetwork connectivity issueAny network operationShow "Network error" with retry option
TIMEOUTOperation timed outAny operationShow timeout message with retry option
UNKNOWN_ERRORCatch-all for unexpected errorsAny operationShow generic error message, log to error tracker

Timeout Durations

OperationTimeout
connect(), signTransaction(), signMessage()5 minutes
Other SDK requests (e.g., selectAccount(), getAccounts())30 seconds
Iframe initialization (initialize())10 seconds
Session check (autoConnect)3 seconds

Handling Connection Errors

The connect() method throws when the user rejects the connection, the wallet times out, or the iframe fails to load. Always wrap it in a try/catch:

import { useState } from 'react';
import { useWallet } from '@kea-wallet/react-sdk';
import { ErrorCode } from '@kea-wallet/browser-sdk';

function ConnectWithErrorHandling() {
const { connect, isConnecting } = useWallet();
const [error, setError] = useState<string | null>(null);

const handleConnect = async () => {
setError(null);
try {
await connect({ metadata: { appName: 'My App' } });
} catch (err) {
const error = err as Error & { code?: string };
switch (error.code) {
case ErrorCode.USER_REJECTED:
// User cancelled — not a real error
console.log('User cancelled the connection');
break;
case ErrorCode.ALREADY_CONNECTED:
// Already connected — safe to ignore
break;
case ErrorCode.TIMEOUT:
setError('Connection timed out. Please try again.');
break;
default:
setError(`Connection failed: ${error.message}`);
}
}
};

return (
<div>
<button onClick={handleConnect} disabled={isConnecting}>
{isConnecting ? 'Connecting...' : 'Connect Wallet'}
</button>
{error && <p className="error">{error}</p>}
</div>
);
}

Key points:

  • connect() throws on user rejection — always wrap in try/catch or the promise rejection will be unhandled.
  • USER_REJECTED is expected behavior — the user chose to cancel. Don't show alarming error UI for this.
  • Concurrent calls are deduplicated — if connect() is already in progress, a second call joins the same request rather than opening a second modal.
  • connect() must be triggered by a user gesture (click/tap) — browsers may block the modal overlay if called programmatically on page load.

Handling Transaction Errors

Signing operations (signTransaction() and signMessage()) can fail for additional reasons: the wallet may not be connected, the payload may be malformed, or the user may reject the signing request.

import { useWallet, useKea } from '@kea-wallet/react-sdk';
import { ErrorCode } from '@kea-wallet/browser-sdk';

function SignWithErrorHandling() {
const { isConnected } = useWallet();
const { sdk } = useKea();

const handleSign = async () => {
if (!isConnected || !sdk) {
console.error('Wallet not connected');
return;
}

try {
const result = await sdk.thru.signTransaction(base64Payload);
console.log('Signed:', result.signedTransaction);
} catch (err) {
const error = err as Error & { code?: string };
switch (error.code) {
case ErrorCode.USER_REJECTED:
console.log('User declined to sign');
break;
case ErrorCode.INVALID_TRANSACTION:
console.error('Invalid transaction format');
break;
case ErrorCode.INSUFFICIENT_FUNDS:
console.error('Not enough balance');
break;
case ErrorCode.TRANSACTION_FAILED:
console.error('Transaction failed on-chain:', error.message);
break;
default:
console.error('Signing failed:', error.message);
}
}
};
}

Key points:

  • Always check isConnected before signing — calling signTransaction() without a connection throws a generic error.
  • Transaction payloads must be base64-encoded strings. Invalid encoding triggers INVALID_TRANSACTION.
  • signMessage() accepts a string or number[] (byte array) — no base64 encoding required for messages.
  • The user can reject signing just like connection — same USER_REJECTED code.
  • Signing operations have a 5-minute timeout before throwing TIMEOUT.

Error & Disconnect Events

Some errors happen in the background — not as a result of calling an SDK method. The SDK emits events for these cases. You should subscribe to them to keep your UI in sync.

Three Event Types

import { useEffect } from 'react';
import { useKea } from '@kea-wallet/react-sdk';

function ErrorEventListener() {
const { sdk } = useKea();

useEffect(() => {
if (!sdk) return;

const onError = (err: Error) => {
console.error('Unrecoverable SDK error:', err.message);
// Show a "wallet unavailable" UI
};

const onLock = ({ reason }: { reason?: string }) => {
console.log('Wallet locked:', reason);
// A disconnect event will follow automatically
};

const onDisconnect = ({ reason }: { reason?: string }) => {
if (reason === 'locked') {
console.log('Disconnected because wallet was locked');
} else if (reason === 'disconnected_in_other_tab') {
console.log('Disconnected from another tab');
} else {
console.log('User disconnected');
}
};

sdk.on('error', onError as never);
sdk.on('lock', onLock as never);
sdk.on('disconnect', onDisconnect as never);

return () => {
sdk.off('error', onError as never);
sdk.off('lock', onLock as never);
sdk.off('disconnect', onDisconnect as never);
};
}, [sdk]);
}

The Lock → Disconnect Cascade

When the wallet is locked (by the user or due to inactivity), two events fire in quick succession:

  1. lock fires with { reason?: string }
  2. disconnect fires immediately after with reason: "locked"
tip

Listen to disconnect for general session-end cleanup. Only listen to lock if you need a specific "Wallet Locked" UI — otherwise disconnect covers both cases.

Disconnect Reasons

ReasonTriggerTypical action
"locked"Wallet was locked (preceded by a lock event)Show "Wallet locked" banner with reconnect option
"disconnected_in_other_tab"User disconnected in another browser tabInformational banner — user chose to disconnect elsewhere
undefinedExplicit disconnect() callClean state transition, no banner needed

React Error Handling

In React apps, KeaProvider handles SDK event subscriptions internally. However, there are two independent error sources you need to handle:

  1. useKea().error — Unrecoverable SDK errors from KeaProvider's internal event listeners (the error event). These represent infrastructure failures like iframe communication breakdowns.
  2. Promise rejections from connect() — User-facing errors like rejection and timeouts. These must be caught via try/catch in your component.
warning

useKea().error and connect() rejections are independent. A try/catch around connect() does not populate useKea().error, and useKea().error does not capture connect() rejections.

State Priority Rendering

When building your UI, check states in this order to show the most relevant information:

isReconnecting → isConnecting → error → connectError → isConnected → default
import { useState } from 'react';
import { useWallet, useKea } from '@kea-wallet/react-sdk';

function WalletStatus() {
const { connect, disconnect, isConnected, isConnecting, isReconnecting } = useWallet();
const { error } = useKea();
const [connectError, setConnectError] = useState<Error | null>(null);

const handleConnect = async () => {
setConnectError(null);
try {
await connect({ metadata: { appName: 'My App' } });
} catch (err) {
setConnectError(err as Error);
}
};

// Priority-based rendering
if (isReconnecting) return <div>Restoring session...</div>;
if (isConnecting) return <div>Connecting...</div>;
if (error) return <div>SDK Error: {error.message}</div>;
if (connectError) return <div>Connection failed: {connectError.message}</div>;
if (isConnected) return <button onClick={() => disconnect()}>Disconnect</button>;
return <button onClick={handleConnect}>Connect Wallet</button>;
}

Key points:

  • Check isReconnecting before isConnecting — this distinguishes silent auto-reconnect from user-initiated connection, avoiding a brief flash of the connect button on page load.
  • useWallet() does not expose an error field — use useKea() for SDK-level errors.
  • For manual event subscriptions in React, always clean up in useEffect's return function to prevent memory leaks and stale closures. See the SDK Events guide for details.

Recovery Patterns

Retry with Exponential Backoff

For transient failures (timeouts, network errors), retry the connection with increasing delays. Never retry USER_REJECTED — the user intentionally cancelled.

import { useWallet } from '@kea-wallet/react-sdk';
import { ErrorCode } from '@kea-wallet/browser-sdk';

async function connectWithRetry(
connect: ReturnType<typeof useWallet>['connect'],
maxAttempts = 3,
) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await connect({ metadata: { appName: 'My App' } });
} catch (err) {
const error = err as Error & { code?: string };

// Never retry user rejections
if (error.code === ErrorCode.USER_REJECTED) {
throw error;
}

if (attempt === maxAttempts) throw error;

// Exponential backoff: 1s, 2s, 3s
await new Promise((r) => setTimeout(r, 1000 * attempt));
}
}
}

Graceful Degradation

Design your UI to work at three levels: full access when connected, read-only when disconnected, and a fallback when the SDK itself has an error:

import { useWallet, useKea } from '@kea-wallet/react-sdk';

function AppContent() {
const { isConnected, isReconnecting, connect } = useWallet();
const { error } = useKea();

if (error) {
return (
<div>
<p>Wallet unavailable. Some features are disabled.</p>
{/* Show read-only public data */}
</div>
);
}

if (isConnected) {
return (
<div>
{/* Full access: signing, transactions, account info */}
</div>
);
}

return (
<div>
{/* Read-only mode: public data, connect prompt */}
<button
onClick={() => connect({ metadata: { appName: 'My App' } })}
disabled={isReconnecting}
>
{isReconnecting ? 'Restoring session...' : 'Connect for full access'}
</button>
</div>
);
}

Reconnect After Disconnect

Show a contextual banner when the wallet disconnects, with a reconnect option. The disconnect reason tells you what happened:

import { useEffect, useRef, useState } from 'react';
import { useWallet, useKea } from '@kea-wallet/react-sdk';

function DisconnectBanner() {
const { isConnected, connect } = useWallet();
const { sdk } = useKea();
const wasConnected = useRef(false);
const [banner, setBanner] = useState<string | null>(null);

useEffect(() => {
if (isConnected) {
wasConnected.current = true;
setBanner(null);
} else if (wasConnected.current) {
wasConnected.current = false;
}
}, [isConnected]);

useEffect(() => {
if (!sdk) return;
const onDisconnect = ({ reason }: { reason?: string }) => {
if (reason === 'locked') {
setBanner('Wallet locked. Please unlock and reconnect.');
} else if (reason === 'disconnected_in_other_tab') {
setBanner('Disconnected from another tab.');
} else {
setBanner('Wallet disconnected.');
}
};
sdk.on('disconnect', onDisconnect as never);
return () => { sdk.off('disconnect', onDisconnect as never); };
}, [sdk]);

if (!banner) return null;

return (
<div className="banner">
<span>{banner}</span>
<button onClick={() => connect({ metadata: { appName: 'My App' } })}>
Reconnect
</button>
<button onClick={() => setBanner(null)}>Dismiss</button>
</div>
);
}
note

The SDK's autoConnect: true already handles session restoration on page reload — you don't need to implement reconnection for that case. The patterns above are for mid-session disconnects (lock, cross-tab disconnect).

UX Best Practices

  • Don't alarm users on USER_REJECTED — it's normal user behavior. Dismiss silently or show a brief, neutral toast.
  • Show actionable error messages — tell users what to do, not just what went wrong. "Please unlock your wallet" is better than "WALLET_LOCKED error".
  • Provide a cancel/retry option for long operations — the 5-minute timeout for connect() and signing is generous. Don't block the UI with a spinner the entire time.
  • Log UNKNOWN_ERROR to an error tracker — these usually indicate edge cases in SDK–iframe communication worth investigating. Consider sending them to Sentry or a similar service.
  • Clean up event listeners — in vanilla JS, always call sdk.off() when done to prevent memory leaks and duplicate notifications. Store the exact handler reference — anonymous functions cannot be unsubscribed.
  • Handle the lock event — a locked wallet is effectively disconnected. Don't leave the UI in a "connected" state when the wallet can no longer sign.

What's Next