Your query looks correct – yet the Supabase JS client throws:
JSON object requested, multiple (or no) rows returned
code: PGRST116
This almost always comes from .single() (or .maybeSingle() misuse) when PostgREST did not get exactly one row. Here is how to fix it cleanly.
The Problem: .single() Means Exactly One Row
.single() tells PostgREST: return one JSON object. If the result set is empty or has more than one row, you get PGRST116 instead of data.
Common causes:
- No matching row (filter too strict, wrong id, RLS hid the row)
- Multiple rows match (missing unique filter)
- Using
.single()on a list endpoint by accident
Empty results under RLS often feel like “the row is missing” – double-check policies and auth.uid(). For insert-time RLS errors see new row violates row-level security policy.
The Solution: Pick the Right Expectation
Step 1: Use maybeSingle When Zero Rows Are OK
const { data, error } = await supabase
.from('profiles')
.select('id, display_name')
.eq('id', userId)
.maybeSingle()
if (error) {
console.error(error.code, error.message)
return
}
if (!data) {
// no profile yet – create one or show onboarding
}
maybeSingle() returns null for zero rows and still errors if multiple rows match.
Step 2: Keep .single() Only for Guaranteed Unique Rows
// OK when id is primary key and the row must exist
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', session.user.id)
.single()
If the row might not exist yet, prefer maybeSingle() or handle PGRST116 explicitly.
Step 3: Fix Filters When Multiple Rows Match
// Too loose → multiple rows → PGRST116
await supabase.from('orders').select('*').eq('user_id', userId).single()
// Better: unique key or limit + explicit choice
await supabase
.from('orders')
.select('*')
.eq('id', orderId)
.eq('user_id', userId)
.maybeSingle()
Step 4: Separate “No Row” From Real Errors
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', userId)
.maybeSingle()
if (error) throw error
if (!data) {
await supabase.from('profiles').insert({ id: userId })
}
Common Mistakes
- Treating PGRST116 as a network failure: it is a result-shape mismatch.
- Assuming the row is missing when RLS blocked SELECT: grants/policies can hide rows – see also permission denied for table (42501).
- Chaining .single() after inserts without SELECT policy: related to the classic RLS insert failure on return.
Troubleshooting Checklist
- ✅ Is zero rows a valid outcome? →
maybeSingle() - ✅ Filter unique enough (primary/unique key)?
- ✅ RLS SELECT policy allows the row for this user?
- ✅ Not accidentally querying a one-to-many relation with
.single()?
Conclusion
PGRST116 means PostgREST did not get exactly one row for a singular response. Use maybeSingle() when absence is normal, tighten filters when multiples appear, and verify RLS if “missing” rows should be visible.
Related articles
- Fix RLS: new row violates row-level security policy
- Fix permission denied for table (42501)
- How to query users table?
- Supabase Auth: Invalid login credentials
- Fix infinite recursion detected in policy
Still seeing PGRST116? Share the query shape (table + filters, no secrets) in a comment.
Comments