Skip to content

Commit bfe2154

Browse files
authored
feat(sdk): add proper error handling (#15148)
Fixes #14495 The SDK now throws a proper `PayloadSDKError` class on failed API requests instead of either returning undefined or a generic `Error`. Error handling is centralized in the `request()` method, which adds error handling for operations where it was previously missing (like `create`, `update`, `delete`, `find`, etc.). **Changes:** - Added `PayloadSDKError` class with `status`, `errors`, `response`, and `message` properties - Centralized error handling in `request()` - all operations now properly throw on failed responses. Previously, some operations (like `findByID`) just threw a generic Error that did not explain what actually went wrong and some operations (like `create`) did not throw an error at all and simply returned undefined. - Lots of new int tests **Usage:** ```ts import { PayloadSDKError } from '@payloadcms/sdk' try { await sdk.create({ collection: 'posts', data: { ... } }) } catch (err) { if (err instanceof PayloadSDKError) { console.log(err.status) // 400 console.log(err.errors) // [{ name: 'ValidationError', message: '...', data: {...} }] } } ```
1 parent d3d8b4e commit bfe2154

11 files changed

Lines changed: 405 additions & 29 deletions

File tree

packages/payload/src/config/types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,16 @@ export type FetchAPIFileUploadOptions = {
699699
useTempFiles?: boolean | undefined
700700
} & Partial<BusboyConfig>
701701

702-
export type ErrorResult = { data?: any; errors: unknown[]; stack?: string }
702+
export type ErrorResult = {
703+
data?: any
704+
errors: {
705+
data?: Record<string, unknown>
706+
field?: string
707+
message?: string
708+
name?: string
709+
}[]
710+
stack?: string
711+
}
703712

704713
export type AfterErrorResult = {
705714
graphqlResult?: GraphQLFormattedError

packages/payload/src/utilities/formatErrors.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ export const formatErrors = (incoming: { [key: string]: unknown } | APIError): E
1818
return {
1919
errors: [
2020
{
21-
name: incoming.name,
22-
data: incoming.data,
23-
message: incoming.message,
21+
name: incoming.name as string,
22+
data: incoming.data as Record<string, unknown>,
23+
message: incoming.message as string,
2424
},
2525
],
2626
}
@@ -52,7 +52,7 @@ export const formatErrors = (incoming: { [key: string]: unknown } | APIError): E
5252
return {
5353
errors: [
5454
{
55-
message: incoming.message,
55+
message: incoming.message as string,
5656
},
5757
],
5858
}

packages/sdk/src/collections/findByID.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,13 @@ export async function findByID<
7878
path: `/${options.collection}/${options.id}`,
7979
})
8080

81-
if (response.ok) {
82-
return response.json()
83-
} else {
84-
throw new Error()
85-
}
86-
} catch {
81+
return response.json()
82+
} catch (err) {
8783
if (options.disableErrors) {
8884
// @ts-expect-error generic nullable
8985
return null
9086
}
9187

92-
throw new Error(`Error retrieving the document ${options.collection}/${options.id}`)
88+
throw err
9389
}
9490
}

packages/sdk/src/collections/findVersionByID.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,13 @@ export async function findVersionByID<
7979
path: `/${options.collection}/versions/${options.id}`,
8080
})
8181

82-
if (response.ok) {
83-
return response.json()
84-
} else {
85-
throw new Error()
86-
}
87-
} catch {
82+
return response.json()
83+
} catch (err) {
8884
if (options.disableErrors) {
8985
// @ts-expect-error generic nullable
9086
return null
9187
}
9288

93-
throw new Error(`Error retrieving the version document ${options.collection}/${options.id}`)
89+
throw err
9490
}
9591
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { ErrorResult } from 'payload'
2+
3+
/**
4+
* Error class for SDK API errors.
5+
* Contains the HTTP status code and error details from the API response.
6+
*/
7+
export class PayloadSDKError extends Error {
8+
/**
9+
* The error data from the API response.
10+
* For ValidationError, this contains `collection`, `global`, and `errors` array.
11+
*/
12+
errors: ErrorResult['errors']
13+
14+
/** The response object */
15+
response: Response
16+
17+
/** HTTP status code */
18+
status: number
19+
20+
constructor({
21+
errors,
22+
message,
23+
response,
24+
status,
25+
}: {
26+
errors: ErrorResult['errors']
27+
message: string
28+
response: Response
29+
status: number
30+
}) {
31+
super(message)
32+
this.name = 'PayloadSDKError'
33+
this.status = status
34+
this.errors = errors
35+
this.response = response
36+
}
37+
}

packages/sdk/src/globals/findVersionByID.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,13 @@ export async function findGlobalVersionByID<
6767
path: `/globals/${options.slug}/versions/${options.id}`,
6868
})
6969

70-
if (response.ok) {
71-
return response.json()
72-
} else {
73-
throw new Error()
74-
}
75-
} catch {
70+
return response.json()
71+
} catch (err) {
7672
if (options.disableErrors) {
7773
// @ts-expect-error generic nullable
7874
return null
7975
}
8076

81-
throw new Error(`Error retrieving the version document ${options.slug}/${options.id}`)
77+
throw err
8278
}
8379
}

packages/sdk/src/index.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import type { ApplyDisableErrors, PaginatedDocs, SelectType, TypeWithVersion } from 'payload'
1+
import type {
2+
ApplyDisableErrors,
3+
ErrorResult,
4+
PaginatedDocs,
5+
SelectType,
6+
TypeWithVersion,
7+
} from 'payload'
8+
9+
export { PayloadSDKError } from './errors/PayloadSDKError.js'
210

311
import type { ForgotPasswordOptions } from './auth/forgotPassword.js'
412
import type { LoginOptions, LoginResult } from './auth/login.js'
@@ -51,6 +59,7 @@ import {
5159
type UpdateManyOptions,
5260
type UpdateOptions,
5361
} from './collections/update.js'
62+
import { PayloadSDKError } from './errors/PayloadSDKError.js'
5463
import { findGlobal, type FindGlobalOptions } from './globals/findOne.js'
5564
import { findGlobalVersionByID } from './globals/findVersionByID.js'
5665
import { findGlobalVersions } from './globals/findVersions.js'
@@ -310,6 +319,31 @@ export class PayloadSDK<T extends PayloadGeneratedTypes = PayloadGeneratedTypes>
310319

311320
const response = await this.fetch(`${this.baseURL}${path}${buildSearchParams(args)}`, init)
312321

322+
if (!response.ok) {
323+
let errorData: {
324+
message?: string
325+
} & Partial<ErrorResult> = {}
326+
327+
try {
328+
errorData = await response.json()
329+
} catch {
330+
// Response body may not be JSON
331+
}
332+
333+
const errors: ErrorResult['errors'] = errorData.errors ?? [
334+
{ message: errorData.message ?? response.statusText },
335+
]
336+
337+
const message = errors[0]?.message ?? response.statusText
338+
339+
throw new PayloadSDKError({
340+
errors,
341+
message,
342+
response,
343+
status: response.status,
344+
})
345+
}
346+
313347
return response
314348
}
315349

test/sdk/collections/Emails.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
export const emailsSlug = 'emails'
4+
5+
export const EmailsCollection: CollectionConfig = {
6+
slug: emailsSlug,
7+
access: { create: () => true, update: () => true, delete: () => true, read: () => true },
8+
fields: [
9+
{
10+
name: 'email',
11+
type: 'email',
12+
unique: true,
13+
required: true,
14+
},
15+
{
16+
name: 'name',
17+
type: 'text',
18+
},
19+
],
20+
}

test/sdk/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const dirname = path.dirname(filename)
55

66
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
77
import { devUser } from '../credentials.js'
8+
import { EmailsCollection } from './collections/Emails.js'
89
import { PostsCollection } from './collections/Posts.js'
910
import { Users } from './collections/Users.js'
1011

@@ -17,6 +18,7 @@ export default buildConfigWithDefaults({
1718
collections: [
1819
Users,
1920
PostsCollection,
21+
EmailsCollection,
2022
{
2123
access: { create: () => true, read: () => true, update: () => true },
2224
slug: 'media',

0 commit comments

Comments
 (0)