Handle phases & errors
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.
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().
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,
asset_integrity_failed, worker_error, wallet_locked, and
runtime_lock_unavailable (the browser refused or cancelled the runtime lock
request itself, which says nothing about another tab; offer a plain retry).
Daemon-originated errors currently use wavelength_error.
wallet_locked is worth handling on its own, and it belongs on the path that
starts the runtime rather than on a payment. Despite the name it has nothing to
do with the locked phase: that one means a wallet on this device is waiting
for its password or passkey, while this means another tab owns the runtime.
The daemon’s browser storage is exclusive to one runtime per origin, so
starting the wallet while another tab already runs it fails immediately with
this code. It is an expected condition
rather than a fault: tell the user to close the other tab, and keep a retry
affordance so they can continue once they have.
import { useState } from 'react';import { useWallet, type RuntimeConfig, type WavelengthError,} from '@lightninglabs/wavelength-react';
function StartButton({ config }: { config: RuntimeConfig }) { const { start } = useWallet(); const [notice, setNotice] = useState('');
const run = async () => { try { await start(config); } catch (err) { // Branch on the code property rather than instanceof: a duplicate // bundled copy of core fails instanceof across the bundle boundary. if ((err as WavelengthError)?.code === 'wallet_locked') { setNotice('Already open in another tab. Close it, then try again.'); return; } setNotice('The wallet runtime could not start.'); } };
return ( <> <button onClick={run}>Start wallet</button> {notice && <p role="alert">{notice}</p>} </> );}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; case 'asset_integrity_failed': console.error('Hosted assets do not match the pinned release.'); break; default: console.error('Payment failed:', err.message); } } } };
return ( <> <button onClick={pay} disabled={sendPending}>Pay</button> {sendError && <p role="alert">{sendError.message}</p>} </> );}