> ## Documentation Index
> Fetch the complete documentation index at: https://vlyai-1c28d863.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Error handling

> Error boundaries, retry policies, user-facing error UX. Patterns for failing gracefully.

Things fail: networks, third-party APIs, user input. The patterns:

## Three layers

<Steps>
  <Step title="Validation at the boundary">
    Convex schema validators reject malformed data before it touches your handler. Add business-rule validation in mutations.
  </Step>

  <Step title="Structured errors in functions">
    Throw with meaningful messages: `throw new Error("Email already in use")`. The client receives the message.
  </Step>

  <Step title="UX-friendly handling on the client">
    Catch errors at component boundaries; show a user-readable message; offer recovery actions.
  </Step>
</Steps>

## Server-side patterns

```typescript theme={null}
export const create = mutation({
  args: { email: v.string() },
  handler: async (ctx, { email }) => {
    if (!isValidEmail(email)) {
      throw new ConvexError({ code: "invalid_email", message: "Please use a valid email address." });
    }

    const existing = await ctx.db.query("users").withIndex("by_email", q => q.eq("email", email)).unique();
    if (existing) {
      throw new ConvexError({ code: "already_exists", message: "An account with this email already exists." });
    }

    // ...
  },
});
```

`ConvexError` is structured — the client sees `code` and `message` separately.

## Client-side patterns

```tsx theme={null}
const create = useMutation(api.users.create);

async function handleSubmit(values) {
  try {
    await create(values);
    toast.success("Account created");
  } catch (err) {
    if (err.data?.code === "already_exists") {
      form.setError("email", { message: "Already taken." });
    } else {
      toast.error(err.message ?? "Something went wrong.");
    }
  }
}
```

## Error boundaries

For UI-level failures (a render error):

```tsx theme={null}
import { ErrorBoundary } from "react-error-boundary";

<ErrorBoundary fallback={<ErrorFallback />}>
  <MyComponent />
</ErrorBoundary>
```

vly's default project structure ships with one error boundary at the app shell.

## Retry policy

For transient failures:

| Where | Pattern |
| - | - |
| Convex mutations called from client | Manual — show error, let user retry |
| Action calling third-party API | `try { ... } catch { wait; retry up to N }` |
| Background jobs | Job table with `attempts`, exponential backoff |

## Tips

<Tip>
  **Don't swallow errors.** A `catch (err) { /* nothing */ }` hides bugs. At minimum, log to Sentry.
</Tip>

<Tip>
  **Fail loudly in dev, gracefully in prod.** Sentry + a toast + a clean UI fallback is the right default.
</Tip>

## Related

<CardGroup cols={3}>
  <Card title="Input validation" icon="check-circle" href="/security/input-validation">
    The first defense.
  </Card>

  <Card title="Sentry integration" icon="alert-triangle" href="/integrations/sentry">
    Error tracking.
  </Card>

  <Card title="Troubleshooting" icon="life-buoy" href="/troubleshooting/overview">
    Common errors.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.