Handle phases & errors

3 min read
Guides
Web SDK

React to wallet phases

phase from useWallet() describes where the wallet is in its lifecycle. Use a switch or a series of conditionals at the root of your app to render the right screen for each phase. The value updates reactively, so the UI transitions automatically as the wallet moves through phases.

Phase Meaning
loading The runtime assets have not finished loading.
runtimeReady The runtime is usable, but the daemon has not started.
starting start() has been called and has not yet resolved.
needsWallet No wallet exists yet; create or restore one.
locked A wallet exists but is locked; unlock it.
syncing The wallet is unlocked and catching up with the chain.
restoring A background restore (useWalletRestore()) is bringing a freshly restored wallet up; setting recoverState: true on the request additionally tracks the recovery scan through the recovery state.
ready The wallet is unlocked and ready to use.
stopping stop() has been called and has not yet resolved.
stopped The daemon has stopped.
error The runtime failed to start, start()/stop() failed, or a background process (sync poll, activity stream, refresh) exhausted its failure budget.

info.walletState (from useWalletInfo()) carries the underlying WalletState string (none, locked, ready, or syncing) when you need the daemon-level detail instead of the broader RuntimePhase.

phases.tsx
import { useWallet } from '@lightninglabs/wavelength-react';
function WalletGate({ children }: { children: React.ReactNode }) {
const { phase } = useWallet();
switch (phase) {
case 'loading':
case 'runtimeReady':
case 'starting':
return <p>Starting wallet runtime...</p>;
case 'needsWallet':
return <CreateWalletScreen />;
case 'locked':
return <UnlockScreen />;
case 'syncing':
case 'restoring':
return <p>Syncing with the wallet server...</p>;
case 'ready':
return <>{children}</>;
case 'stopping':
case 'stopped':
return <p>Wallet stopped.</p>;
case 'error':
return <p>Runtime failed to start.</p>;
default:
return null;
}
}

A failed start() or stop() moves phase straight to 'error'; there is no separate 'startFailed'/'stopFailed' phase to branch on. Read error from useWallet() for the reason.

The Error | null convention

Every mutation hook (useWalletCreate, useWalletRestore, useWalletUnlock, useWalletDeposit, useWalletReceive, useWalletPrepareSend, useWalletSend, useWalletRefresh, and the create/open pair on useWalletPasskey) exposes the same throw-and-capture shape: an action function, a verb-prefixed <verb>Pending: boolean, a verb-prefixed <verb>Error: Error | null, and (except useWalletRefresh and useWalletPasskey, neither of which exposes a <verb>Data field) a verb-prefixed <verb>Data, the last successful result. The error field is always Error | null, never a string, so .message is always safe to read. Calling the action clears the previous error/data, awaits the underlying engine call, and on completion both updates the hook’s state and settles the returned promise the normal way: resolves with the result, or rejects with the same Error instance that lands in the error field. So you can read the error field for a declarative render, or await/try/catch the action for imperative flow, whichever fits the call site. reset<Verb>() clears the pending/error/data trio back to idle without calling anything.

error at the top level, from useWallet(), is the same Error | null shape but tracks fatal runtime-level failures instead of a single mutation: it is set when the initial runtime load fails, when start()/stop() fails, or after a background process exhausts its failure budget, and it clears on the next successful start().

errors.tsx
import { useWalletSend } from '@lightninglabs/wavelength-react';
function PayButton({ bolt11 }: { bolt11: string }) {
const { send, sendError, sendPending, resetSend } = useWalletSend();
const pay = async () => {
resetSend();
try {
await send({ invoice: bolt11 });
} catch (err) {
// sendError is already set for a declarative render; err is the same
// Error instance for imperative handling right here.
console.error('Payment failed:', err);
}
};
return (
<>
<button onClick={pay} disabled={sendPending}>Pay</button>
{sendError && <p role="alert">{sendError.message}</p>}
</>
);
}

Interpret error codes

Every error the Wavelength SDK originates extends WavelengthError and carries a code string that identifies the failure reason at the machine level. Match on err.code to show a specific message rather than a generic fallback. Named SDK codes include runtime_not_ready, asset_load_failed, and worker_error; daemon-originated errors currently use wavelength_error.

errors.tsx
import { useWalletSend, WavelengthError } from '@lightninglabs/wavelength-react';
function PayButton({ bolt11 }: { bolt11: string }) {
const { send, sendError, sendPending, resetSend } = useWalletSend();
const pay = async () => {
resetSend();
try {
await send({ invoice: bolt11 });
} catch (err) {
if (err instanceof WavelengthError) {
switch (err.code) {
case 'runtime_not_ready':
console.error('Runtime still loading.');
break;
case 'asset_load_failed':
console.error('Check runtimeBaseUrl and hosted assets.');
break;
default:
console.error('Payment failed:', err.message);
}
}
}
};
return (
<>
<button onClick={pay} disabled={sendPending}>Pay</button>
{sendError && <p role="alert">{sendError.message}</p>}
</>
);
}