Skip to content

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.

Markdown
On this page

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#

TypeScript
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 reported

captureResult(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:

FieldValue
Error type and titlethe type below, for example db.permission_denied, with operation: message
Messagethe database's message, scrubbed like every other text and cut to 300 characters
Tagserror.type, db.system.name (postgresql), db.operation.name, db.response.status_code — names from OpenTelemetry's database conventions
Groupingby 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.typeCodesBy default
db.permission_denied42501one event
db.unique_violation23505breadcrumb, no event
db.foreign_key_violation23503one event
db.query_canceled57014one event
db.serialization_failure40001, 40P01one event
db.no_rowsPGRST116breadcrumb, no event
db.jwt_expiredPGRST301one event
db.request_invalidPGRST2xxone event
db.errorany other codeone 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:

TypeScript
// 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):

db.mjs
// 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#

  • captureResult needs a started SDK (init, withBelvador, or Sentry.init on an official SDK). Without one it returns the result and reports BLV_NOT_INITIALIZED once — 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 captureException are reported twice if you also wrap the result — pick one.

Spotted something wrong or missing? Tell us: hello@belvador.dev