learn.sol
Week 4 • Frontend Transaction Ux Status Errors Recovery

Frontend Transaction UX: Status, Errors, Recovery

Implement a strict transaction UX state machine so users always know what is happening and what to do next.

Step 1: 0-to-1 Theory

Primitives

  • status machine: controlled UI states for tx lifecycle
  • error category: actionable error bucket
  • recovery action: explicit next step users can take

Mental Model

Good transaction UX behaves like a checkout flow:

  • it never double-charges
  • it always shows where you are
  • it always tells you what to do if something fails

That is exactly what a simple state machine gives you.

Invariants

  1. Every failure state must expose one next action.
  2. In-flight actions must disable duplicate submission.
  3. Success must show signature.

Quick Checks

  1. Why is a state machine better than random booleans?
  2. Why should success always include signature?

Step 2: Real-World Implementation (Code Solution)

Create lib/solana/uiTxState.ts:

// Keep status values small and explicit so the UI cannot drift.
export type TxStatus =
  | "idle"
  | "preparing"
  | "awaiting-signature"
  | "broadcasting"
  | "confirming"
  | "confirmed"
  | "failed";

// Error kinds are user-facing categories. The raw error stays in logs.
export type TxErrorKind = "user-rejected" | "network" | "expired" | "program" | "unknown";

export function classifyTxError(err: string): TxErrorKind {
  const e = err.toLowerCase();
  // The goal is not perfect parsing. The goal is actionable UX.
  if (e.includes("reject")) return "user-rejected";
  if (e.includes("blockhash")) return "expired";
  if (e.includes("timeout") || e.includes("network")) return "network";
  if (e.includes("instruction") || e.includes("program")) return "program";
  return "unknown";
}

Create components/solana/TxActionCard.tsx:

"use client";

import React, { useState } from "react";
import { classifyTxError, type TxStatus } from "@/lib/solana/uiTxState";

export function TxActionCard() {
  // Keep tx UI state in a single place so it is impossible to show conflicting statuses.
  const [status, setStatus] = useState<TxStatus>("idle");
  const [signature, setSignature] = useState("");
  const [error, setError] = useState("");

  const run = async () => {
    // Clear previous attempt output.
    setError("");
    setSignature("");

    try {
      // These status transitions are the UX contract.
      setStatus("preparing");
      setStatus("awaiting-signature");
      setStatus("broadcasting");
      setStatus("confirming");
      // Replace with real service call:
      const fakeSig = "3Ab...demo-signature";
      setSignature(fakeSig);
      setStatus("confirmed");
    } catch (e: any) {
      const kind = classifyTxError(String(e?.message ?? e));
      // Always show a next action to the user.
      setError(`Failed (${kind}). Suggested action: retry or rebuild.`);
      setStatus("failed");
    }
  };

  return (
    <div className="rounded-xl border border-zinc-700 p-4 space-y-2">
      <div className="text-sm">Status: {status}</div>
      <button onClick={run} disabled={status !== "idle" && status !== "failed" && status !== "confirmed"} className="px-3 py-2 rounded bg-cyan-700 text-white">
        Run Transaction
      </button>
      {signature && <div className="text-xs">Signature: {signature}</div>}
      {error && <div className="text-xs text-red-300">{error}</div>}
    </div>
  );
}

Expected result:

  • deterministic status progression and actionable error output.

Step 3: Mastery Test

  • Easy: where do we prevent double-submit?
  • Medium: how do we map raw errors to actionable categories?
  • Hard: how would you persist tx status across route navigation?

Sources

Solana Assistant

AI-powered documentation helper

Welcome to Solana Assistant

Ask specific questions about Solana development:

Ask specific questions for better results400px
    Frontend Transaction UX: Status, Errors, Recovery | learn.sol