GenericTryCatch

Prev Next

GenericTryCatch

GenericTryCatch is a structured error boundary for async operations — a single static method that wraps try/catch/finally with loading state, error handling, and fallback semantics.

It complements AsyncHandler (which normalizes errors at API boundaries) by providing the same discipline for UI-initiated async work like data fetching, form submissions, and side effects.

API

import { GenericTryCatch } from '@/lib/internal/GenericTryCatch';

// Void variant — no return value, re-throws on error
await GenericTryCatch.run({
  run: async () => {
    await doSomething();
  },
  setLoading,
  onError: (err) => showToast(err.message),
  onFinally: () => cleanup(),
});

// Value variant — returns T on success, fallback on error
const result = await GenericTryCatch.run({
  run: async () => {
    return await fetchData();
  },
  fallback: [],
  setLoading,
  onError: (err) => showToast(err.message),
});

Method Signature

// Void — no fallback, re-throws normalized error
static async run(options: GenericTryCatchVoidOptions): Promise<void>;

// Value — fallback provided, swallows error and returns fallback
static async run<T>(options: GenericTryCatchValueOptions<T>): Promise<T>;

The presence or absence of fallback determines the overload:

fallback provided? On success On error Return type
No Returns void Calls onError, then re-throws Promise<void>
Yes Returns T Calls onError, returns fallback Promise<T>

Options

Base Options (shared)

Option Type Required Description
setLoading (loading: boolean) => void No Called with true before run, false in finally
onError (error: Error) => void No Called with the normalized error
onFinally () => void | Promise<void> No Called in finally after setLoading(false)

Void Options

Option Type Required Description
run () => Promise<void> Yes The async operation to execute

Value Options

Option Type Required Description
run () => Promise<T> Yes The async operation to execute
fallback T | (() => T) Yes Static value or factory returned on error

Execution Flow

GenericTryCatch-08-26-2026_01_04_PM (1).png

Error Normalization

GenericTryCatch normalizes unknown errors internally before passing them to onError:

function normalizeError(err: unknown): Error {
  if (err instanceof Error) return err;
  return new Error(typeof err === 'string' ? err : 'Unknown error');
}

This is a lightweight normalization — it produces Error instances, not AppError. When the run function already throws AppError (e.g., from services that use AsyncHandler), the AppError passes through as-is since it extends Error.

Fallback Resolution

Fallbacks can be static values or factories:

// Static fallback
fallback: [];

// Factory fallback — called lazily on error
fallback: () => getDefaultConfig();

Factories are useful when the default value is expensive to construct or should be created fresh on each error.

Usage in Practice

The auth hook uses GenericTryCatch for operations that return data:

const refreshTenantList = useCallback(async () => {
  return await GenericTryCatch.run({
    setLoading,
    onError: (err) => onMessage(err.message, { variant: 'error' }),
    fallback: [],
    run: async () => {
      const deviceId = CookieManager.get('deviceId');
      if (!deviceId) throw AppError.create('auth.device_id_missing');
      const res = await IdentityService.refreshTenantList(deviceId);
      return res.tenants;
    },
  });
}, [commonOptions, onMessage]);

On success, the caller gets res.tenants. On failure, the error is shown as a toast and the caller gets [] — no unhandled promise rejection, no crashed UI.

Relationship to Other Layers

AsyncHandler

  • AsyncHandler wraps service and API boundaries — ensures only AppError escapes
  • GenericTryCatch wraps UI-initiated async work — provides loading/error/fallback ergonomics

They complement each other: a service call wrapped by AsyncHandler may throw AppError, which GenericTryCatch catches and routes to onError.

AppError

  • GenericTryCatch does not depend on AppError directly
  • It normalizes unknownError, which preserves AppError instances (since AppError extends Error)
  • Callers inside run are free to throw AppError — it flows through correctly

Design Decisions

  • Single static method — avoids class instantiation; the options object is the entire configuration surface
  • Overloaded by fallback presence — the type system enforces that void operations cannot accidentally swallow errors; if you want error recovery, you must provide a fallback
  • setLoading in both try and finally — guarantees loading state is always cleaned up, even on error
  • Lightweight normalization — converts unknown to Error without requiring AppError, keeping GenericTryCatch usable outside of service boundaries
  • Lazy fallback factoriesfallback: () => T defers construction to error time, avoiding unnecessary allocation on the happy path