Error handling
Every helper exposes failures as an error: Signal<Error | undefined> and moves status() to
'error'. Imperative helpers do that and reject their promise, so you can handle a failure
declaratively in the template, imperatively at the call site, or both.
Queries
Section titled “Queries”A query has no call site, so its only failure channel is reactive:
readonly todos = injectQuery(api.todos.list, () => ({ count: 20 }), { onError: (err) => console.error(err),});| Surface | Behavior on failure |
|---|---|
error() |
Holds the Error. Cleared when a later result succeeds. |
status() |
'error'. |
data() |
The last real value is preserved, so the UI does not blank out. |
onError |
Called with the Error. |
Mutations and actions
Section titled “Mutations and actions”mutate() and run() update the reactive state and then reject. The rejection carries the same
Error instance that lands on error().
readonly completeTodo = injectMutation(api.todos.complete);
async complete(id: TodoId) { await this.completeTodo.mutate({ id }); // rejects on failure}Because the promise rejects, an unhandled call produces an unhandled rejection. Choose a channel deliberately:
- Reactive only — render
error()in the template and swallow the rejection at the call site withtry/catch, orvoid promise.catch(() => {}). - Imperative only —
catchand handle it, ignoring the signal. - Both — the common case:
catchfor control flow (stay on the form, focus a field) anderror()for display.
| Surface | Behavior on failure |
|---|---|
error() |
Holds the Error. |
status() |
'error'. |
data() |
Cleared at the start of the call and not repopulated. |
onError |
Called with the Error, before the promise rejects. |
| the promise | Rejects with the same Error instance. |
reset() clears data, error, and returns the status to 'idle' — useful after navigating away
from a failed form.
Non-Error throws are normalized
Section titled “Non-Error throws are normalized”Convex functions can throw anything. Imperative helpers normalize whatever comes back before it touches your code:
const errorObj = err instanceof Error ? err : new Error(String(err));So error() is always an Error (never a string, null, or a bare object), onError always
receives an Error, and the promise always rejects with an Error. A thrown 'boom' becomes
new Error('boom'); the original value survives only as the message text.
Narrowing with ConvexError
Section titled “Narrowing with ConvexError”Convex distinguishes an application error — thrown deliberately by your function with
new ConvexError(payload) — from a transport or unexpected error. ConvexError extends Error and
carries the payload on .data.
convex-angular re-exports ConvexError so you can narrow helper errors without reaching into
convex/values:
import { ConvexError } from 'convex-angular';import { ConvexError } from 'convex/values';
export const complete = mutation({ args: { id: v.id('todos') }, handler: async (ctx, args) => { const todo = await ctx.db.get(args.id); if (!todo) { throw new ConvexError({ code: 'NOT_FOUND', id: args.id }); } await ctx.db.patch(args.id, { completed: true }); return null; },});import { Component, computed } from '@angular/core';import { ConvexError, injectMutation, injectQuery } from 'convex-angular';
import { TodoId, api } from './convex/api';
@Component({ selector: 'app-todo-errors', template: ` @if (todos.error(); as error) { <p role="alert">{{ error.message }}</p> } @if (validationMessage(); as message) { <p role="alert">{{ message }}</p> } `,})export class TodoErrorsComponent { readonly todos = injectQuery(api.todos.list, () => ({ count: 20 })); readonly completeTodo = injectMutation(api.todos.complete);
// `ConvexError` is re-exported by convex-angular so you can narrow helper // errors without importing from `convex/values` directly. readonly validationMessage = computed(() => { const error = this.todos.error(); return error instanceof ConvexError ? String(error.data) : undefined; });
async complete(id: TodoId): Promise<void> { try { // The rejection and `completeTodo.error()` carry the same Error instance. await this.completeTodo.mutate({ id }); } catch (error) { if (error instanceof ConvexError) { // Application error thrown by your Convex function: `.data` is the // payload you passed to `new ConvexError(...)`. console.error('rejected by the backend', error.data); } else if (error instanceof Error) { // Anything else — including a non-Error throw, which the helper // normalizes to `new Error(String(err))`. console.error(error.message); } } }}instanceof ConvexError is the only reliable narrowing — do not parse error.message. An error that
is not a ConvexError is a transport failure, an argument-validation failure, or an uncaught
exception in your function, and should generally be reported rather than shown verbatim to the user.
Related
Section titled “Related”- Status and state — how
'error'is derived and what it hides. - Reactivity — generation and version guarding of stale failures.