@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]>
OutcomeTupleNotes
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');