BugInfrastructure

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))…

Rootlock SRE Engine 6 min read
Diagnostic brief

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.

Severity Not rated
Confidence Medium
Frequency Unknown
Impact See analysis

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:

  1. The SDK resolves with an { error } field (no window.freighterApi bridge) → connectWallet catches this and re-throws an Error.
  2. The SDK itself throws an Error because window.freighterApi is undefined → the thrown value is again instanceof 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

  1. Import isFreighterInstalled from @stellar/freighter-api in src/lib/wallet.ts (or directly in WalletContext.tsx, depending on preferred separation of concerns).
  1. Add an explicit extension-presence check at the top of connectWallet (or at the call site in connect) 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.

  1. Update the catch block in WalletContext.tsx#connect to branch on the sentinel rather than on instanceof:
   } 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.

  1. Remove or verify the now-dead !(err instanceof Error) fallback branch. If no other path can produce a non-Error throw, replace it with a generic safe fallback as shown above rather than leaving misleading intent in the code.
  1. Audit connectWallet to confirm no failure path produces a non-Error throw. If any throw statement uses a raw string or object, normalize it to throw new Error(...).

CLI Commands

Locate all throw sites in the wallet layer to audit for non-Error throws:

grep -n "throw " src/lib/wallet.ts

Confirm 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 FreighterNotInstalledError

Confirm 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-Error throws: Add an ESLint rule (no-throw-literal) to enforce that only Error instances (or subclasses) are thrown throughout the codebase, ensuring future catch blocks that use instanceof Error behave predictably.
  { "rules": { "no-throw-literal": "error" } }
  • Test coverage for the not-installed path: Add a dedicated test case that mocks isFreighterInstalled returning false and 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.ts module with domain-specific error subclasses (FreighterNotInstalledError, WalletPermissionDeniedError, etc.) so error discrimination in UI layers never depends on string matching or instanceof Error with type-unsafe branches.
  • SDK version pinning: Pin @stellar/freighter-api to a specific minor version in package.json and 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.
Developer FirstBuilt for engineers solving real problems
Evidence DrivenTechnical claims tied to available evidence
Automation ReadyStructured for CLI, APIs, and workflows
Privacy FocusedNo unnecessary data collection in this article UI
STAY AHEAD OF ISSUES

Get new root-cause analyses in your inbox

Engineering-focused updates. No fake subscriber counts. Unsubscribe anytime.