Skip to main content

Documentation Index

Fetch the complete documentation index at: https://confect.dev/llms.txt

Use this file to discover all available pages before exploring further.

A Confect function can declare a typed error in its spec. When the handler fails with a value matching that schema, the error travels across the function boundary and surfaces—already decoded—at every call site (React hooks, JS clients, and tests). Typed errors are transported as Convex’s native ConvexError, with the encoded error stored in ConvexError.data. The returns channel is left untouched, and non-Confect callers of the same Convex API see ordinary ConvexErrors. For mutations, throwing a ConvexError triggers Convex’s transaction rollback as usual. Error schemas are optional. When omitted, the error type defaults to never and every call site keeps its prior shape.

Declaring an error schema

Add an error field to any FunctionSpec constructor. The schema is an ordinary Effect Schema, so a Schema.TaggedError class works well for a single failure mode.
confect/notes.spec.ts
import { FunctionSpec, GenericId, GroupSpec } from "@confect/core";
import { Schema } from "effect";

import { Notes } from "./tables/Notes";

export class NoteNotFound extends Schema.TaggedError<NoteNotFound>()(
  "NoteNotFound",
  { noteId: GenericId.GenericId("notes") },
) {}

export const notes = GroupSpec.make("notes").addFunction(
  FunctionSpec.publicQuery({
    name: "getOrFail",
    args: Schema.Struct({ noteId: GenericId.GenericId("notes") }),
    returns: Notes.Doc,
    error: NoteNotFound,
  }),
);
To declare more than one failure mode, combine them with Schema.Union.
import { Schema } from "effect";

export class NoteNotFound extends Schema.TaggedError<NoteNotFound>()(
  "NoteNotFound",
  { noteId: GenericId.GenericId("notes") },
) {}

export class Forbidden extends Schema.TaggedError<Forbidden>()(
  "Forbidden",
  {},
) {}

FunctionSpec.publicMutation({
  name: "deleteOrFail",
  args: Schema.Struct({ noteId: GenericId.GenericId("notes") }),
  returns: Schema.Null,
  error: Schema.Union(NoteNotFound, Forbidden),
});

Failing from a handler

When a spec declares an error schema, the corresponding FunctionImpl handler’s Effect error channel is typed as that schema’s Type. Use Effect.fail (or Effect.mapError) to fail with any value matching the schema.
confect/notes.impl.ts
import { FunctionImpl } from "@confect/server";
import { Effect } from "effect";

import api from "./_generated/api";
import { DatabaseReader } from "./_generated/services";
import { NoteNotFound } from "./notes.spec";

const getOrFail = FunctionImpl.make(
  api,
  "notes",
  "getOrFail",
  ({ noteId }) =>
    Effect.gen(function* () {
      const reader = yield* DatabaseReader;

      return yield* reader
        .table("notes")
        .get(noteId)
        .pipe(Effect.mapError(() => new NoteNotFound({ noteId })));
    }),
);
Without an error schema, the handler’s error channel is never and any unmapped failure must still be cleared (typically with Effect.orDie). With one, declared failures travel through, and any other failure (a defect) still rejects/dies as before.

Consuming typed errors

The decoded error is surfaced at every call site. The exact shape depends on the client.

@confect/react

  • useQuery returns a QueryResult<A, E>. Match it with QueryResult.match, where onFailure is required when the ref declares an error schema.
  • useMutation and useAction return Promise<Either<A, E>> when the ref declares an error schema (and Promise<A> otherwise).
See React for full examples.
import { QueryResult, useQuery } from "@confect/react";
import refs from "../confect/_generated/refs";

const lookup = useQuery(refs.public.notes.getOrFail, { noteId });

QueryResult.match(lookup, {
  onLoading: (skipped) => (skipped ? null : "Looking up…"),
  onSuccess: (note) => `Found: ${note.text}`,
  onFailure: (error) => `Note ${error.noteId} not found.`,
});

@confect/js

Both HttpClient and WebSocketClient add the decoded error to the returned Effect’s error channel, alongside the existing transport error and ParseError.
HttpClient.query(refs.public.notes.getOrFail, { noteId });
// Effect.Effect<Note, NoteNotFound | HttpClientError | ParseError>
WebSocketClient.reactiveQuery carries typed errors too: when the subscribed query fails with a typed error, the returned Stream terminates with the decoded value in its error channel. See HTTP and WebSocket.

@confect/test

TestConfect’s query, mutation, and action decode typed errors into the Effect error channel, so test bridges see the same shape as the JS clients. See Testing.

Non-typed failures

Failures that are not declared in the error schema are not silently swallowed. They propagate through Convex’s normal failure path: they reject promises in @convex/react, fail Effects with HttpClientError/WebSocketClientError in @confect/js, and surface as defects (cause) in TestConfect. Use a typed error schema for failures you want callers to handle, and let everything else die.