Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1571,6 +1571,10 @@ export const api: NavMenuConstant = {
{ name: 'Generating TypeScript Types', url: '/guides/api/rest/generating-types' },
{ name: 'Generating Python Types', url: '/guides/api/rest/generating-python-types' },
{ name: 'Error Codes', url: '/guides/api/rest/postgrest-error-codes' },
{
name: 'Handling Errors in supabase-js',
url: '/guides/api/handling-errors-in-supabase-js',
},
],
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Deno.serve(async (req) => {
// Supabase API URL - env var exported by default when deployed.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase API SECRET KEY - env var exported by default when deployed.
Deno.env.get(SUPABASE_SECRET_KEYS['default']) ?? ''
SUPABASE_SECRET_KEYS['default'] ?? ''
)

// Construct image url from storage
Expand Down
145 changes: 145 additions & 0 deletions apps/docs/content/guides/api/handling-errors-in-supabase-js.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
---
id: handling-errors-in-supabase-js
title: 'Handling errors in `supabase-js`'
subtitle: 'Read `error.hint` first — Postgres often tells you the exact fix. Log the full error so you actually see it.'
---

Every `supabase-js` call returns a `{ data, error }` pair instead of throwing. When something fails, the single most useful field on `error` is usually `hint` — Postgres returns the _fix_, not just a description of the problem. Logging only `error.message` hides it.

## Usage of `message` and `hint` properties

Consider a `42501` permission-denied error on a table where default `GRANT`s have been revoked from `anon`:

```
message: "permission denied for table users"
hint: "Grant the required privileges to the current role with: GRANT SELECT ON public.users TO anon;"
```

The `message` exposes the error reason, and `hint` gives you the literal SQL statement to run in the dashboard SQL editor to fix it.

The same pattern shows up across many Postgres errors — missing column? `hint` suggests the column name you probably meant. Type mismatch? `hint` shows the expected type. Whenever Postgres knows the fix, it puts it in `hint`.

<Admonition type="tip">Log the full `error` object, not just `error.message`.</Admonition>

## The recommended pattern

Read `{ data, error }` from the response, check `error`, log the whole object, and return early.

```ts
const { data, error } = await supabase.from('users').select()
if (error) {
console.error(error)
return
}
```

In the case of a permission-denied error, the response body will look like this:

```json
{
"error": {
"code": "42501",
"message": "permission denied for table users",
"details": null,
"hint": "Grant the required privileges to the current role with: GRANT SELECT ON public.users TO anon;"
},
"status": 401,
"statusText": "Unauthorized"
}
```

`postgrest-js` passes the body through verbatim, so `error.hint` is the exact string Postgres produced. Treat it as the answer the database is giving you, not as a suggestion to file away.

## The `PostgrestError` fields, by usefulness

Database calls (`select`, `insert`, `update`, `upsert`, `delete`, `rpc`) return a `PostgrestError` with four fields. Read them in roughly this order:

| Field | Read it when |
| --------- | ------------------------------------------------------------------------------------------------------------------ |
| `hint` | Always check first. When Postgres includes one, it's the actionable fix (a `GRANT` to run, a column name, a type). |
| `code` | When branching in code. Codes are stable across versions; `message` text isn't. |
| `details` | When `hint` and `message` aren't enough. Often contains the offending value, key, or row. |
| `message` | As the human summary. Useful in UI strings, less useful for debugging. |

A full list of PostgREST error codes is in the [Error Codes reference](/guides/api/rest/postgrest-error-codes).

## Branch on `error.code`, not `error.message`

`error.code` is more reliable than `error.message` for programmatic branching: messages change between Postgres and PostgREST versions, but codes are stable.

```ts
const { data, error } = await supabase.from('users').select()
if (error) {
console.error(error)
if (error.code === '42501') {
// Permission denied. error.hint usually contains the GRANT to run.
}
return
}
```

## Errors from Auth, Storage, and Edge Functions

The same rule applies across the SDK — log the whole error object — but the shape differs by client.

### Auth

`AuthError` exposes `error.code` (e.g. `'invalid_credentials'`, `'email_not_confirmed'`) and `error.status`. Branch on `code`; log the whole thing.

```ts
const { data, error } = await supabase.auth.signInWithPassword({
email: 'example@email.com',
password: 'example-password',
})
if (error) {
console.error(error)
return
}
```

### Storage

`StorageError` exposes `error.statusCode` (HTTP status as a string) and a structured `error` name (e.g. `'Duplicate'`, `'NotFound'`).

```ts
const { data, error } = await supabase.storage
.from('avatars')
.upload('public/avatar1.png', avatarFile)
if (error) {
console.error(error)
return
}
```

### Edge Functions

Functions errors arrive as one of three subclasses. Narrow with `instanceof`; for `FunctionsHttpError`, parse the body to get the function's own error payload.

```ts
import { FunctionsFetchError, FunctionsHttpError, FunctionsRelayError } from '@supabase/supabase-js'

const { data, error } = await supabase.functions.invoke('hello')
if (error instanceof FunctionsHttpError) {
console.error('Function error', await error.context.json())
} else if (error) {
console.error(error)
}
```

### Realtime

The `subscribe()` callback receives a `status` and, on failure, an `err` argument. Log the whole `err` — its `cause` often holds the underlying reason.

```ts
supabase.channel('room1').subscribe((status, err) => {
if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
console.error(status, err)
}
})
```

## Related

- [PostgREST Error Codes](/guides/api/rest/postgrest-error-codes)
- [Automatic retries with `supabase-js`](/guides/api/automatic-retries-in-supabase-js)
- [Securing your API](/guides/api/securing-your-api)
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ const elevenLabsClient = new ElevenLabsClient({
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabase = createClient(
Deno.env.get('SUPABASE_URL') || '',
Deno.env.get(SUPABASE_SECRET_KEYS['default']) || ''
SUPABASE_SECRET_KEYS['default'] || ''
)

async function scribe({
Expand Down
4 changes: 3 additions & 1 deletion apps/docs/content/guides/platform/access-control.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ The table below shows the actions each role can take on the resources belonging
| Production Branch | Read | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> |
| | Write | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> |
| Development Branches | List | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> |
| | Create | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> |
| | Create[^8] | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> |
| | Update | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> |
| | Delete | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> |

Expand All @@ -281,3 +281,5 @@ The table below shows the actions each role can take on the resources belonging
[^6]: Listed permissions are for the API and Dashboard.

[^7]: Limited to executing SELECT queries. SQL Query Snippets run by the Read-Only role are run against the database using the **supabase_read_only_user**. This role has the [predefined Postgres role pg_read_all_data](https://www.postgresql.org/docs/current/predefined-roles.html).

[^8]: When using dashboard branching without a GitHub integration, the first branch creation also registers the project's production branch — a one-time step that requires Owner or Administrator. See [Branching via the dashboard](/docs/guides/deployment/branching/dashboard) for details. Developers can create, update, and delete branches normally after that.
79 changes: 50 additions & 29 deletions apps/docs/features/directives/Partial.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { describe, it, expect } from 'vitest'

import { mdxToMarkdown } from 'mdast-util-mdx'
import { toMarkdown } from 'mdast-util-to-markdown'
import { describe, expect, it } from 'vitest'

import { partialsRemark } from './Partial'
import { fromDocsMarkdown } from './utils.server'
Expand Down Expand Up @@ -116,7 +115,12 @@ Some more text.
await expect(partialsRemark()(mdast)).rejects.toThrowError(/valid JSON/)
})

it('should error when required variable is missing', async () => {
it('should render an unprovided variable as an empty string', async () => {
// The variables.mdx fixture reads "Here is a partial that takes a {{ .var }}."
// When `var` is not provided, the `{{ .var }}` placeholder is replaced with
// an empty string rather than throwing. The trailing " ." in the expected
// output is the intended result: the placeholder is gone, leaving nothing
// between "a" and the period.
const markdown = `
# Embed partial

Expand All @@ -126,54 +130,64 @@ Some more text.
`.trim()

const mdast = fromDocsMarkdown(markdown)
await expect(partialsRemark()(mdast)).rejects.toThrowError(
/Missing required variable in \$Partial ".*variables\.mdx": "var"/
)
})
const transformed = await partialsRemark()(mdast)
const output = toMarkdown(transformed, { extensions: [mdxToMarkdown()] })

it('should error when unexpected variable is provided', async () => {
const markdown = `
// Note the empty gap where `{{ .var }}` used to be — this is deliberate.
const expected = `
# Embed partial

<$Partial path="/_fixtures/variables.mdx" variables={{ "var": "correct", "extra": "unexpected" }} />
Here is a partial that takes a .

Some more text.
`.trim()
`.trimStart()

const mdast = fromDocsMarkdown(markdown)
await expect(partialsRemark()(mdast)).rejects.toThrowError(
/Unexpected variable in \$Partial ".*variables\.mdx": "extra"/
)
expect(output).toEqual(expected)
// The placeholder must be fully removed, not left as literal `{{ .var }}`.
expect(output).not.toContain('{{')
})

it('should error with detailed message for multiple missing variables', async () => {
it('should ignore a variable that is not referenced in the partial', async () => {
// The variables.mdx fixture only references `var`. Providing an additional
// `extra` variable that the partial never uses is silently ignored rather
// than throwing — `var` is substituted and `extra` leaves no trace.
const markdown = `
# Embed partial

<$Partial path="/_fixtures/multiple-variables.mdx" variables={{ "var1": "value1" }} />
<$Partial path="/_fixtures/variables.mdx" variables={{ "var": "correct", "extra": "unused" }} />

Some more text.
`.trim()

const mdast = fromDocsMarkdown(markdown)
await expect(partialsRemark()(mdast)).rejects.toThrowError(
/Missing required variables.*"var2".*"var3".*Expected variables.*"var1".*"var2".*"var3".*Provided variable: "var1"/s
)
const transformed = await partialsRemark()(mdast)
const output = toMarkdown(transformed, { extensions: [mdxToMarkdown()] })

expect(output).toContain('Here is a partial that takes a correct.')
expect(output).not.toContain('unused')
})

it('should error with detailed message for multiple unexpected variables', async () => {
it('should render only the unprovided variables as empty when some are provided', async () => {
// The multiple-variables.mdx fixture reads:
// "This partial has {{ .var1 }}, {{ .var2 }}, and {{ .var3 }}."
// Only `var1` is provided here, so `var2` and `var3` collapse to empty
// strings while `var1` is substituted normally.
const markdown = `
# Embed partial

<$Partial path="/_fixtures/variables.mdx" variables={{ "var": "correct", "extra1": "wrong", "extra2": "also wrong" }} />
<$Partial path="/_fixtures/multiple-variables.mdx" variables={{ "var1": "value1" }} />

Some more text.
`.trim()

const mdast = fromDocsMarkdown(markdown)
await expect(partialsRemark()(mdast)).rejects.toThrowError(
/Unexpected variables.*"extra1".*"extra2".*Expected variable: "var".*Provided variables.*"var".*"extra1".*"extra2"/s
)
const transformed = await partialsRemark()(mdast)
const output = toMarkdown(transformed, { extensions: [mdxToMarkdown()] })

// Provided variable is substituted; the two unprovided ones leave empty gaps.
expect(output).toContain('This partial has value1, , and .')
// No placeholder text survives for the unprovided variables.
expect(output).not.toContain('{{')
})

it('should succeed when all variables match exactly', async () => {
Expand Down Expand Up @@ -212,7 +226,12 @@ Some more text.
expect(output).toContain('alphanumeric value')
})

it('should error when hyphenated variable is missing', async () => {
it('should render unprovided hyphenated variables as empty', async () => {
// The hyphenated-variables.mdx fixture reads:
// "This partial has {{ .my-var }}, {{ .another_var }}, and {{ .myVar123 }}."
// Only `my-var` is provided, so the underscore and alphanumeric variables
// collapse to empty strings — confirming the empty-substitution behavior
// applies to all supported variable name styles.
const markdown = `
# Embed partial

Expand All @@ -222,8 +241,10 @@ Some more text.
`.trim()

const mdast = fromDocsMarkdown(markdown)
await expect(partialsRemark()(mdast)).rejects.toThrowError(
/Missing required variables.*"another_var".*"myVar123".*Expected variables.*"my-var".*"another_var".*"myVar123".*Provided variable: "my-var"/s
)
const transformed = await partialsRemark()(mdast)
const output = toMarkdown(transformed, { extensions: [mdxToMarkdown()] })

expect(output).toContain('This partial has value, , and .')
expect(output).not.toContain('{{')
})
})
Loading
Loading