Skip to content
This repository was archived by the owner on Oct 9, 2025. It is now read-only.

Commit 9df1e84

Browse files
authored
feat: Implement unwrap() to throw query errors instead of returning them (#188)
* Implement unwrap() to throw query errors instead of returning them The rationale and motivation for this change is well described in supabase/supabase-js#92, and further supported by the discussion in supabase/supabase#604. This implementation doesn't actually unwrap the result as described in supabase/supabase-js#92, i.e. returning only the actual data. This is done deliberately for two reasons: 1. Making sure the caller can still extract `count`, `status` and `statusText` while still benefitting from errors being thrown instead of returned 2. Ease the transition from the non-throwing pattern where destructuring is norm anyway, so less code has to be rewritten to take advantage of unwrap Basic tests were added to test/basic.ts and the unwrap function is documented to some degree at least, though this can probably be improved. * Rename the unwrap API to throwOnError instead The `unwrap` moniker was a bit of a misnomer, since it didn't actually unwrap the result, and this should make things a bit more clear. This was discussed in a bit more detail in #188.
1 parent 5ac1216 commit 9df1e84

File tree

3 files changed

+57
-1
lines changed

3 files changed

+57
-1
lines changed

src/lib/types.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,23 @@ export abstract class PostgrestBuilder<T> implements PromiseLike<PostgrestRespon
5454
protected headers!: { [key: string]: string }
5555
protected schema?: string
5656
protected body?: Partial<T> | Partial<T>[]
57+
protected shouldThrowOnError?: boolean
5758

5859
constructor(builder: PostgrestBuilder<T>) {
5960
Object.assign(this, builder)
6061
}
6162

63+
/**
64+
* If there's an error with the query, throwOnError will reject the promise by
65+
* throwing the error instead of returning it as part of a successful response.
66+
*
67+
* {@link https://github.com/supabase/supabase-js/issues/92}
68+
*/
69+
throwOnError(): PostgrestBuilder<T> {
70+
this.shouldThrowOnError = true
71+
return this
72+
}
73+
6274
then<TResult1 = PostgrestResponse<T>, TResult2 = never>(
6375
onfulfilled?:
6476
| ((value: PostgrestResponse<T>) => TResult1 | PromiseLike<TResult1>)
@@ -92,7 +104,8 @@ export abstract class PostgrestBuilder<T> implements PromiseLike<PostgrestRespon
92104
const isReturnMinimal = this.headers['Prefer']?.split(',').includes('return=minimal')
93105
if (this.method !== 'HEAD' && !isReturnMinimal) {
94106
const text = await res.text()
95-
if (text && text !== '' && this.headers['Accept'] !== 'text/csv') data = JSON.parse(text)
107+
if (text && text !== '' && this.headers['Accept'] !== 'text/csv')
108+
data = JSON.parse(text)
96109
}
97110

98111
const countHeader = this.headers['Prefer']?.match(/count=(exact|planned|estimated)/)
@@ -102,6 +115,10 @@ export abstract class PostgrestBuilder<T> implements PromiseLike<PostgrestRespon
102115
}
103116
} else {
104117
error = await res.json()
118+
119+
if (error && this.shouldThrowOnError) {
120+
throw error
121+
}
105122
}
106123

107124
const postgrestResponse: PostgrestResponse<T> = {
@@ -112,6 +129,7 @@ export abstract class PostgrestBuilder<T> implements PromiseLike<PostgrestRespon
112129
statusText: res.statusText,
113130
body: data,
114131
}
132+
115133
return postgrestResponse
116134
})
117135
.then(onfulfilled, onrejected)

test/__snapshots__/index.test.ts.snap

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,8 @@ Object {
626626
}
627627
`;
628628

629+
exports[`connection errors should work the same with throwOnError 1`] = `[FetchError: request to http://this.url.does.not.exist/user?select=* failed, reason: getaddrinfo ENOTFOUND this.url.does.not.exist]`;
630+
629631
exports[`don't mutate PostgrestClient.headers 1`] = `null`;
630632

631633
exports[`embedded filters embedded eq 1`] = `
@@ -2262,3 +2264,12 @@ Object {
22622264
"statusText": "OK",
22632265
}
22642266
`;
2267+
2268+
exports[`throwOnError throws errors instead of returning them 1`] = `
2269+
Object {
2270+
"code": "42P01",
2271+
"details": null,
2272+
"hint": null,
2273+
"message": "relation \\"public.missing_table\\" does not exist",
2274+
}
2275+
`;

test/basic.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,19 @@ test('missing table', async () => {
9595
expect(res).toMatchSnapshot()
9696
})
9797

98+
test('throwOnError throws errors instead of returning them', async () => {
99+
let isErrorCaught = false
100+
101+
try {
102+
await postgrest.from('missing_table').select().throwOnError()
103+
} catch (error) {
104+
expect(error).toMatchSnapshot()
105+
isErrorCaught = true
106+
}
107+
108+
expect(isErrorCaught).toBe(true)
109+
})
110+
98111
test('connection error', async () => {
99112
const postgrest = new PostgrestClient('http://this.url.does.not.exist')
100113
let isErrorCaught = false
@@ -107,6 +120,20 @@ test('connection error', async () => {
107120
expect(isErrorCaught).toBe(true)
108121
})
109122

123+
test('connection errors should work the same with throwOnError', async () => {
124+
const postgrest = new PostgrestClient('http://this.url.does.not.exist')
125+
let isErrorCaught = false
126+
await postgrest
127+
.from('user')
128+
.select()
129+
.throwOnError()
130+
.then(undefined, (error) => {
131+
expect(error).toMatchSnapshot()
132+
isErrorCaught = true
133+
})
134+
expect(isErrorCaught).toBe(true)
135+
})
136+
110137
test('custom type', async () => {
111138
interface User {
112139
username: string

0 commit comments

Comments
 (0)