@void-snippets/core
Utilities
catchError() — Go-style [Error, null] | [null, T] promise handling.
#catchError — Go-style error handling
try/catch breaks the linear flow of async code. You end up declaring a let result above the block or nesting logic inside the try. Neither reads cleanly.
catchError wraps any Promise in a [error, data] tuple. Success and failure both live in the same line.
typescript
async function catchError<T>(
promise: Promise<T>
): Promise<[Error, null] | [null, T]>| Outcome | Tuple | Notes | |
|---|---|---|---|
| Success | [null, T] | T is fully typed — not `T \ | undefined` |
| Failure | [Error, null] | Non-Error rejections are coerced to Error automatically |
#Before and after
typescript
// ❌ Before — noisy, result leaks out of scope
async function saveContact(data: Contact.Apis.Create) {
let contact: Contact.Detail | undefined;
try {
contact = await ContactsApis.create(data);
toast.success('Created!');
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Unknown error');
return;
}
// TypeScript might still think contact is undefined here
navigate(`/contacts/${contact._id}`);
}
// ✅ After — linear, no variable leaking, TypeScript narrows correctly
async function saveContact(data: Contact.Apis.Create) {
const [err, contact] = await catchError(ContactsApis.create(data));
if (err) { toast.error(err.message); return; }
toast.success('Created!');
navigate(`/contacts/${contact._id}`); // contact is Contact.Detail — not undefined
}#Combining with useAsyncState
useAsyncState.execute() returns the same [error, data] tuple, so you can mix the two patterns freely:
typescript
const { execute } = useAsyncState<{ downloadUrl: string }>();
const [err, result] = await execute(() => ContactsApis.export({ format: 'csv' }));
if (result) window.open(result.downloadUrl, '_blank');