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
.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
AsyncHandlerwraps service and API boundaries — ensures onlyAppErrorescapesGenericTryCatchwraps 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
GenericTryCatchdoes not depend onAppErrordirectly- It normalizes
unknown→Error, which preservesAppErrorinstances (sinceAppErrorextendsError) - Callers inside
runare free to throwAppError— 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
setLoadingin both try and finally — guarantees loading state is always cleaned up, even on error- Lightweight normalization — converts
unknowntoErrorwithout requiringAppError, keepingGenericTryCatchusable outside of service boundaries - Lazy fallback factories —
fallback: () => Tdefers construction to error time, avoiding unnecessary allocation on the happy path