Report errors from { data, error } clients
captureResult() reports the errors Supabase, PostgREST and other clients return instead of throwing: one event typed by its Postgres code, grouped by type and operation, never the SQL.
Supabase, PostgREST and many other database clients do not throw: they return { data, error }. Nothing reaches Belvador unless your code reports that error — so a missing permission or a cancelled query fails quietly for every user. captureResult() reports it in one line and hands the result back unchanged. Available from SDK 0.6.0 in every @belvador/* package.
Use it#
import { captureResult } from "@belvador/browser"; // every @belvador/* package exports itconst { data, error } = captureResult( await supabase.from("invoices").select("id, total").eq("customer_id", customerId), { operation: "select invoices" },);if (error) return showRetry(); // the result is unchanged; the error is already reportedcaptureResult(result, { operation }) never throws and never changes the result: your existing if (error) code keeps working. When the call succeeded it does nothing.
operation names the call — select invoices, rpc create_invoice — never the SQL. It is cut to 100 characters on one line.
What is sent#
One error event per failing call:
| Field | Value |
|---|---|
| Error type and title | the type below, for example db.permission_denied, with operation: message |
| Message | the database's message, scrubbed like every other text and cut to 300 characters |
| Tags | error.type, db.system.name (postgresql), db.operation.name, db.response.status_code — names from OpenTelemetry's database conventions |
| Grouping | by type and operation: every permission error of select invoices is one issue, whatever the message says |
Never sent: the SQL, the error's details and hint (they can hold row values), and your data.
Error types#
error.type | Codes | By default |
|---|---|---|
db.permission_denied | 42501 | one event |
db.unique_violation | 23505 | breadcrumb, no event |
db.foreign_key_violation | 23503 | one event |
db.query_canceled | 57014 | one event |
db.serialization_failure | 40001, 40P01 | one event |
db.no_rows | PGRST116 | breadcrumb, no event |
db.jwt_expired | PGRST301 | one event |
db.request_invalid | PGRST2xx | one event |
db.error | any other code | one event |
db.unique_violation and db.no_rows are usually normal flow — a duplicate sign-up, a lookup that found nothing — so by default they leave a breadcrumb that travels with the next real error instead of creating an event. To report every type for one call, pass your own list:
// Here a duplicate email is a bug, not normal flow: report every error type.captureResult(await supabase.from("subscribers").insert({ email }), { operation: "insert subscribers", expected: [],});On an official Sentry SDK#
Servers on @sentry/node, @sentry/bun or @sentry/deno (Supabase Edge Functions too) bind the same captureResult to their own SDK with @belvador/core/sentry (from SDK 0.6.1):
// captureResult on the app's own Sentry SDK (@sentry/node, @sentry/bun or @sentry/deno).import * as Sentry from "@sentry/node";import { captureResultWith } from "@belvador/core/sentry";export const captureResult = captureResultWith(Sentry);It uses your Sentry SDK's own functions, so it works whatever @sentry/* version you run; Belvador tests it with 11.9.0. Everything on this page applies unchanged. On Supabase Edge Functions the shared file already exports it — see Supabase Edge Functions.
Good to know#
captureResultneeds a started SDK (init,withBelvador, orSentry.initon an official SDK). Without one it returns the result and reportsBLV_NOT_INITIALIZEDonce — see Verify the connection.A thrown exception (a network failure inside the client, a bug in your code) is not a result: it is reported the usual way, by the SDK's error handlers or by
captureException.Errors already reported by your own
captureExceptionare reported twice if you also wrap the result — pick one.
Spotted something wrong or missing? Tell us: hello@belvador.dev