WalletContext catch Block Friendly Install Prompt Is Unreachable Due to Guaranteed instanceof Error Catch Path
The connect function in WalletContext.tsx intends to show a user-friendly "Install the Freighter wallet extension to continue." message when the wallet is absent, but the guard condition (!(err instanceof Error))…
At a Glance
The connect function in WalletContext.tsx intends to show a user-friendly "Install the Freighter wallet extension to continue." message when the wallet is absent, but the guard condition (!(err instanceof Error)) is never satisfied in practice.
Summary
The connect function in WalletContext.tsx intends to show a user-friendly "Install the Freighter wallet extension to continue." message when the wallet is absent, but the guard condition (!(err instanceof Error)) is never satisfied in practice. Every failure path in connectWallet throws a real Error object, ensuring the raw SDK or application error message is always displayed to end users instead. The friendly, actionable prompt is effectively dead code.
Root-Cause Analysis
Confirmed evidence:
The catch block in WalletContext.tsx#connect applies the friendly message only when the caught value is not an Error instance:
} catch (err) {
setError(
err instanceof Error
? err.message
: "Install the Freighter wallet extension to continue.",
);
return null;
}connectWallet in src/lib/wallet.ts always throws real Error objects for every failure mode it handles — it never throws a plain string, a plain object, or any other non-Error type:
if (granted.error || !granted.isAllowed) {
throw new Error("Wallet access was not granted.");
}
// ...
if (error || !address) {
throw new Error(error?.message ?? "Unable to read wallet address.");
}Reasonable inference:
The @stellar/freighter-api SDK uses a resolve-with-{ error } pattern rather than throwing for expected failure states (e.g., extension not installed, permission denied). connectWallet already consumes those { error } shaped responses and re-throws them as Error instances before they can propagate into the catch block as non-Error values.
When the Freighter extension is entirely absent, one of two things happens:
- The SDK resolves with an
{ error }field (nowindow.freighterApibridge) →connectWalletcatches this and re-throws anError. - The SDK itself throws an
Errorbecausewindow.freighterApiisundefined→ the thrown value is againinstanceof Error.
Conclusion:
In every reachable failure scenario — extension not installed, permission denied, address unavailable — the caught value satisfies err instanceof Error, so err.message (a raw technical string) is always used. The right-hand branch of the ternary is unreachable.
Contributing factor:
isFreighterInstalled() from @stellar/freighter-api is available but currently unused anywhere in the codebase. This is the correct API to perform an explicit, early detection of the "extension absent" state rather than relying on inferring it from a thrown exception's type.
Resolution Steps
- Import
isFreighterInstalledfrom@stellar/freighter-apiinsrc/lib/wallet.ts(or directly inWalletContext.tsx, depending on preferred separation of concerns).
- Add an explicit extension-presence check at the top of
connectWallet(or at the call site inconnect) before any SDK calls are attempted:
const installed = await isFreighterInstalled();
if (!installed) {
throw new Error("FREIGHTER_NOT_INSTALLED");
}Using a sentinel error message string (rather than a bare human-readable string) lets the catch block make a precise, stable comparison without coupling UI copy to internal error strings.
- Update the
catchblock inWalletContext.tsx#connectto branch on the sentinel rather than oninstanceof:
} catch (err) {
if (err instanceof Error && err.message === "FREIGHTER_NOT_INSTALLED") {
setError("Install the Freighter wallet extension to continue.");
} else {
setError(
err instanceof Error
? err.message
: "An unexpected error occurred. Please try again.",
);
}
return null;
}Alternatively, introduce a custom error subclass (see Configuration Snippets) to avoid magic string comparisons.
- Remove or verify the now-dead
!(err instanceof Error)fallback branch. If no other path can produce a non-Errorthrow, replace it with a generic safe fallback as shown above rather than leaving misleading intent in the code.
- Audit
connectWalletto confirm no failure path produces a non-Errorthrow. If anythrowstatement uses a raw string or object, normalize it tothrow new Error(...).
CLI Commands
Locate all throw sites in the wallet layer to audit for non-Error throws:
grep -n "throw " src/lib/wallet.tsConfirm isFreighterInstalled is exported by the installed SDK version:
node -e "const f = require('@stellar/freighter-api'); console.log(typeof f.isFreighterInstalled);"Verify the symbol is not already imported anywhere:
grep -rn "isFreighterInstalled" src/Configuration Snippets
Optional: Custom error class for type-safe discrimination (preferred over magic strings)
// src/lib/errors.ts
export class FreighterNotInstalledError extends Error {
constructor() {
super("Freighter wallet extension is not installed.");
this.name = "FreighterNotInstalledError";
}
}Updated connectWallet using the custom error:
// src/lib/wallet.ts
import { isFreighterInstalled, isAllowed, setAllowed, getAddress } from "@stellar/freighter-api";
import { FreighterNotInstalledError } from "./errors";
export async function connectWallet() {
const installed = await isFreighterInstalled();
if (!installed) {
throw new FreighterNotInstalledError();
}
// ... existing logic unchanged
}Updated catch block using the custom error:
// src/context/WalletContext.tsx
import { FreighterNotInstalledError } from "@/lib/errors";
} catch (err) {
if (err instanceof FreighterNotInstalledError) {
setError("Install the Freighter wallet extension to continue.");
} else {
setError(
err instanceof Error
? err.message
: "An unexpected error occurred. Please try again.",
);
}
return null;
}Verification
Simulate the extension-absent case:
In a browser profile with the Freighter extension disabled or uninstalled, trigger the wallet connect flow. The UI should now display:
Install the Freighter wallet extension to continue.rather than a raw SDK error string such as "Wallet access was not granted." or a generic JavaScript exception message.
Unit test the sentinel path:
// Example using Jest / vitest
import * as freighterApi from "@stellar/freighter-api";
jest.spyOn(freighterApi, "isFreighterInstalled").mockResolvedValue(false);
const result = await connectWallet();
// Should throw FreighterNotInstalledErrorConfirm no regression on the happy path:
With the extension installed and permission granted, the connect flow must still resolve a valid Stellar address without surfacing any error state.
Rollback indicator:
If users report seeing "Install the Freighter wallet extension to continue." when the extension is installed, isFreighterInstalled() may be returning false incorrectly — roll back the guard check and file a bug against the SDK version in use.
Prevention
- Lint rule for bare non-
Errorthrows: Add an ESLint rule (no-throw-literal) to enforce that onlyErrorinstances (or subclasses) are thrown throughout the codebase, ensuring future catch blocks that useinstanceof Errorbehave predictably.
{ "rules": { "no-throw-literal": "error" } }- Test coverage for the not-installed path: Add a dedicated test case that mocks
isFreighterInstalledreturningfalseand asserts the correct UI error string is set. This makes the branch visible in coverage reports and prevents regression.
- Custom error taxonomy: Maintain a
src/lib/errors.tsmodule with domain-specific error subclasses (FreighterNotInstalledError,WalletPermissionDeniedError, etc.) so error discrimination in UI layers never depends on string matching orinstanceof Errorwith type-unsafe branches.
- SDK version pinning: Pin
@stellar/freighter-apito a specific minor version inpackage.jsonand review the changelog on upgrades. SDK changes to the{ error }resolve pattern vs. thrown exceptions will silently break catch-block assumptions.
- User-facing error message registry: Centralize all user-visible error strings in a single constants file rather than embedding them inline in catch blocks. This makes it straightforward to audit which messages are actually reachable and prevents silent dead-copy accumulation.