Production-ready error handling across the SmartDrop app with clear user feedback and comprehensive error recovery.
The error handling system provides:
- Typed Error Classes: Specialized error classes for different failure scenarios (Freighter, RPC, Contract, Validation, etc.)
- User-Friendly Messages: Clear, actionable error messages (no technical jargon)
- Toast Notifications: Chakra UI toast for consistent, accessible notifications
- Automatic Logging: Development logging with optional production error tracking
- Error Boundaries: React error boundaries to prevent full app crashes
- Retry Logic: Automatic exponential backoff retry for transient failures
- Global Error Handling: Catches unhandled promise rejections and errors
SmartDropError (abstract base)
├── FreighterError
├── RPCError
├── ContractError
├── ValidationError
├── ConfigError
└── UnknownError
Defines all error classes and utilities:
// Error normalization
const error = normalizeError(unknownError, "context");
// Retry with exponential backoff
const result = await withRetry(
() => someAsyncFunction(),
{ maxAttempts: 3, initialDelayMs: 500 }
);
// Error logging
errorLogger.log(error, "context");
// Global error setup
setupGlobalErrorHandlers();React hook for displaying notifications:
const toast = useToast();
// Show notifications
toast.success("Title", "Description");
toast.error("Title", "Description");
toast.info("Title", "Description");
toast.warning("Title", "Description");
// Handle errors (logs + shows user-friendly message)
const error = toast.handleError(err, "context");
// Execute with automatic error handling and success notification
const result = await toast.withErrorHandling(
async () => someOperation(),
{
loadingMessage: "Processing...",
successMessage: "Done!",
errorContext: "Operation Name",
retryConfig: { maxAttempts: 3 }
}
);Provides global access to error handling:
// In any component
const toast = useErrorHandler();
toast.success("Done!");Catches React component errors:
// Wrap entire app or sections
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
// With custom fallback
<ErrorBoundary fallback={(error, retry) => <CustomError />}>
<MyComponent />
</ErrorBoundary>
// Section-level error handling
<ErrorBoundarySection sectionName="Farm">
<FarmComponent />
</ErrorBoundarySection>"use client";
import { useStellarWallet } from "@/context/StellarWalletContext";
import { useErrorHandler } from "@/context/ErrorContext";
export function ConnectButton() {
const { connect } = useStellarWallet();
const toast = useErrorHandler();
const handleConnect = async () => {
try {
await connect();
toast.success("Connected", "Wallet connected successfully");
} catch (error) {
toast.handleError(error, "Wallet Connection");
}
};
return <Button onClick={handleConnect}>Connect</Button>;
}const handleUnlock = async () => {
const result = await toast.withErrorHandling(
async () => unlockAssets(params),
{
loadingMessage: "Processing unlock...",
successMessage: "Assets unlocked successfully!",
errorContext: "Unlock Assets",
retryConfig: { maxAttempts: 3 }
}
);
if (result) {
// Update UI with result
}
};try {
const result = await someAsyncOperation();
} catch (error) {
const normalized = toast.handleError(error, "Operation Context");
// Check error type
if (normalized instanceof FreighterError) {
// Handle wallet issues
} else if (normalized instanceof RPCError) {
// Handle RPC issues
}
}const handleSubmit = async (formData) => {
try {
if (!formData.amount || formData.amount <= 0) {
throw new ValidationError("Amount must be greater than zero");
}
const result = await submitTransaction(formData);
toast.success("Success", "Transaction submitted");
} catch (error) {
toast.handleError(error, "Form Submission");
}
};Wallet connection and signing errors:
FREIGHTER_NOT_INSTALLED: Extension not foundFREIGHTER_REJECTED: User denied connectionFREIGHTER_NETWORK_MISMATCH: Wallet on wrong networkFREIGHTER_TIMEOUT: Extension did not respond before the connection timeoutFREIGHTER_UNKNOWN: Other wallet issues
Is Transient: Only FREIGHTER_TIMEOUT (user can retry after reloading or waking the extension)
Is Critical: Yes (prevents user interaction)
Blockchain RPC endpoint errors:
RPC_TIMEOUT: Request timed outRPC_RATE_LIMIT: Too many requestsRPC_INVALID_RESPONSE: Malformed responseRPC_NETWORK_ERROR: Network connectivity issueRPC_UNKNOWN: Other RPC issues
Is Transient: Timeout, rate limit, network errors (can be retried) Is Critical: No
Smart contract execution errors:
CONTRACT_INSUFFICIENT_BALANCE: User lacks required fundsCONTRACT_AUTHORIZATION_FAILED: Missing permissionsCONTRACT_INVALID_PARAMETERS: Invalid input parametersCONTRACT_EXECUTION_FAILED: Execution failedCONTRACT_NOT_FOUND: Contract doesn't exist
Is Transient: No Is Critical: No
User input validation errors:
- Generic validation messages
Is Transient: No Is Critical: No
Application configuration errors:
- Missing environment variables
Is Transient: No Is Critical: Yes (prevents app function)
Unmapped/unexpected errors
Is Transient: No Is Critical: Yes (unknown impact)
// ✅ Good
const result = await toast.withErrorHandling(
() => someLongOperation(),
{ loadingMessage: "Please wait...", successMessage: "Done!" }
);
// ❌ Avoid
setPending(true);
try {
await someLongOperation();
toast.success("Done!");
} catch (error) {
toast.handleError(error);
}// ✅ Good
toast.handleError(error, "Unlock Assets");
// ❌ Less helpful
toast.handleError(error);// ✅ Good
if (!amount || amount <= 0) {
throw new ValidationError("Amount must be positive");
}
// ❌ Avoid
setError("Amount must be positive");
return;// ✅ Good - App-level
<ErrorBoundary>
<ErrorProvider>
<App />
</ErrorProvider>
</ErrorBoundary>
// ✅ Good - Section-level
<ErrorBoundarySection sectionName="Farm">
<Farm />
</ErrorBoundarySection>
// ❌ Avoid - Too nested
<ErrorBoundary>
<Modal>
<Form>
<ErrorBoundary>
<Input />
</ErrorBoundary>
</Form>
</Modal>
</ErrorBoundary>// ✅ Good for RPC calls
await toast.withErrorHandling(
() => fetchData(),
{
retryConfig: {
maxAttempts: 3,
initialDelayMs: 500,
backoffMultiplier: 2
}
}
);
// ❌ Avoid for non-transient errors
await toast.withErrorHandling(
() => submitForm(),
{ retryConfig: { maxAttempts: 3 } } // Form errors won't be fixed by retry
);- ✅ Error handler library created (
/src/lib/error-handler.ts) - ✅ Toast hook implemented (
/src/hooks/useToast.ts) - ✅ Error context provider created (
/src/context/ErrorContext.tsx) - ✅ Error boundary component created (
/src/components/ErrorBoundary/ErrorBoundary.tsx) - ✅ Context provider updated to include ErrorProvider and ErrorBoundary
- ✅ Wallet context updated to throw proper errors
- ✅ Connect button updated to use toast notifications
- ✅ Unlock modal updated to use error handler
- ✅ Soroban utilities updated to use new error classes
- Errors logged to browser console with full stack traces
- Component stack included for React errors
- Detailed error information displayed in error boundaries
- Errors logged to browser console (optional)
- Errors can be sent to error tracking service (Sentry, LogRocket, etc.)
- User-friendly messages shown (no technical details)
To enable production error tracking:
// In errorLogger.log() method
if (!this.isDevelopment) {
captureException(error, { contexts: { smartdrop: logData } });
}- Browser console - all errors logged in development
- Error boundary fallback UI - component errors caught
- Toast notifications - user-visible errors
Issue: "useErrorHandler must be used within ErrorProvider" Solution: Ensure component is wrapped in ErrorProvider (already done in AppShell)
Issue: Error not caught by boundary Solution: Error boundaries only catch render-time errors, not async errors. Use try/catch for async.
Issue: Same error appearing multiple times Solution: Check if wrapped in multiple ErrorBoundaries or ErrorProviders
- Sentry integration for production error tracking
- Custom error recovery strategies per error type
- Offline error queue with retry on reconnection
- Error analytics and reporting dashboard
- User feedback collection on errors
- Suggested actions for common error scenarios