Result Type

Result Type

Warpkit uses @bitclaw/result for typed error handling in all server functions. Every server function returns a Result<T> instead of throwing.

Why

Throwing errors across the server/client boundary in TanStack Start loses type information: the client receives an opaque error with no structured data. Result<T> makes success and failure explicit in the return type, so the client can handle both without try/catch.

Shape

type Result<T> =
  | { ok: true;  data: T }
  | { ok: false; code: string; message: string }

Server: returning results

import { err, ok } from '@bitclaw/result';
import { ERROR_CODES } from '@/lib/constants';

export const createItem = createServerFn({ method: 'POST' })
  .inputValidator(z.object({ title: z.string() }))
  .handler(async ({ data }) => {
    const user = await requireUser();
    if (!user) return err(ERROR_CODES.UNAUTHORIZED, 'Not authenticated');

    // happy path
    return ok({ id: 'new-id', title: data.title });
  });

err(code, message): returns { ok: false, code, message }

ok(data): returns { ok: true, data }

Client: consuming results

const result = await createItem({ data: { title: 'My item' } });

if (!result.ok) {
  console.error(result.code, result.message);
  return;
}

// result.data is typed as { id: string; title: string }
console.log(result.data.id);

In a route loader:

export const Route = createFileRoute('/_app/items')({
  loader: () => getItems(),
  component: ItemsPage,
});

function ItemsPage() {
  const result = Route.useLoaderData();

  if (!result.ok) return <ErrorBanner message={result.message} />;

  return <ul>{result.data.map(item => <li key={item.id}>{item.title}</li>)}</ul>;
}

Error codes

All codes are defined in src/lib/constants/errors.ts:

UNAUTHORIZED (401), FORBIDDEN (403), NOT_FOUND (404), VALIDATION_ERROR (422),
RATE_LIMITED (429), STRIPE_ERROR (500), NO_SUBSCRIPTION, EMAIL_PROVIDER_NOT_CONFIGURED,
EMAIL_SEND_FAILED, INTERNAL (500), ACCOUNT_DELETION_PENDING, PLAN_LIMIT_EXCEEDED,
STORAGE_NOT_CONFIGURED, NO_CREDITS, EMAIL_DISPOSABLE, EMAIL_DOMAIN_INVALID,
CANCEL_FAILED, RETRY_FAILED

Always use ERROR_CODES.*: never hardcode string literals: so errors are refactorable and greppable.

Server routes

TanStack Start server routes (createFileRoute + server.handlers) return standard Response objects. They don't use Result because they go over the wire as raw JSON, not through TanStack's server function layer.

// Server route: return Response with status code
return Response.json({ error: 'Too many requests' }, { status: 429 });

// Server function: use ok()/err()
return err(ERROR_CODES.RATE_LIMITED, 'Too many requests');