payload icon indicating copy to clipboard operation
payload copied to clipboard

SDK does not throw errors instead returns undefined

Open Siebe-Studio opened this issue 4 months ago • 3 comments

Describe the Bug

SDK package version 3.62.1

When a Payload API request fails (e.g., 400 Bad Request), the PayloadSDK returns undefined instead of throwing an error. This makes it impossible to properly handle API errors and provides a poor developer experience.

Expected Behavior

When an API request fails, the SDK should throw an error that can be caught and handled appropriately. This would allow:

  • Proper error handling in try/catch blocks
  • React Query's onError callback to fire
  • Users to see appropriate error messages instead of success messages

Actual Behavior

The SDK returns undefined when a request fails, causing:

  • No way to easily access error details from the API response
  • Silent failures that are difficult to debug
import { type Config } from '@/payload-types'
import { PayloadSDK } from '@payloadcms/sdk'

export const sdk = new PayloadSDK<Config>({
  baseURL: process.env.NEXT_PUBLIC_SERVER_URL + '/api',
  baseInit: {
    credentials: 'include',
  },
  fetch: typeof window !== 'undefined' ? window.fetch.bind(window) : globalThis.fetch,
})

export default sdk

Image
{
    "errors": [
        {
            "name": "ValidationError",
            "data": {
                "collection": "waitlist-email",
                "errors": [
                    {
                        "message": "Value must be unique",
                        "path": "email"
                    }
                ]
            },
            "message": "The following field is invalid: email"
        }
    ]
}
Image

Link to the code that reproduces this issue

see below

Reproduction Steps

  1. Set up a Payload SDK instance with a custom fetch
  2. Attempt to create a document that violates validation (e.g., duplicate unique field)
  3. Observe that the SDK returns undefined instead of throwing an error
import { PayloadSDK } from '@payloadcms/sdk'

const sdk = new PayloadSDK<Config>({
  baseURL: process.env.NEXT_PUBLIC_SERVER_URL + '/api',
  baseInit: {
    credentials: 'include',
  },
  fetch: typeof window !== 'undefined' ? window.fetch.bind(window) : globalThis.fetch,
})

// This returns undefined when the API returns 400 Bad Request
const response = await sdk.create({
  collection: 'waitlist-email',
  data: {
    email: '[email protected]', // Assuming this email already exists
  },
})

console.log(response) // undefined

API Response

The API correctly returns a 400 Bad Request with error details:

{
  "errors": [
    {
      "name": "ValidationError",
      "data": {
        "collection": "waitlist-email",
        "errors": [
          {
            "message": "Value must be unique",
            "path": "email"
          }
        ]
      },
      "message": "The following field is invalid: email"
    }
  ]
}

However, this error information is lost because the SDK returns undefined instead of throwing an error with this data.

Which area(s) are affected? (Select all that apply)

plugin: other

Environment Info

Binaries:
  Node: 22.14.0
  npm: 10.9.2
  Yarn: N/A
  pnpm: 10.6.5
Relevant Packages:
  payload: 3.62.1
Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:40 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6041
  Available memory (MB): 24576
  Available CPU cores: 12

Siebe-Studio avatar Nov 05 '25 18:11 Siebe-Studio

Please add a reproduction in order for us to be able to investigate.

Depending on the quality of reproduction steps, this issue may be closed if no reproduction is provided.

Why was this issue marked with the invalid-reproduction label?

To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with create-payload-app@latest -t blank or a forked/branched version of this repository with tests added (more info in the reproduction-guide).

To make sure the issue is resolved as quickly as possible, please make sure that the reproduction is as minimal as possible. This means that you should remove unnecessary code, files, and dependencies that do not contribute to the issue. Ensure your reproduction does not depend on secrets, 3rd party registries, private dependencies, or any other data that cannot be made public. Avoid a reproduction including a whole monorepo (unless relevant to the issue). The easier it is to reproduce the issue, the quicker we can help.

Please test your reproduction against the latest version of Payload to make sure your issue has not already been fixed.

I added a link, why was it still marked?

Ensure the link is pointing to a codebase that is accessible (e.g. not a private repository). "example.com", "n/a", "will add later", etc. are not acceptable links -- we need to see a public codebase. See the above section for accepted links.

Useful Resources

github-actions[bot] avatar Nov 05 '25 18:11 github-actions[bot]

Underlying cause is because the various methods (find, global, etc) effectively

  1. fetch from the backend
  2. return the response.json() promise

They never check response.ok. If you want it to throw instead and want a quick and dirty workaround, you could patch the package to modify the request method that they all call to throw an error response if not ok, and use the cause for the status.

https://github.com/payloadcms/payload/blob/87137febd4311f3cded321542cd1c8a163147254/packages/sdk/src/index.ts#L275-L314

I think the real fix is to change the API response to include this information. For example, the api package (which generates libraries from swagger defs), returns:

{
  data: isJson ? await response.json() : await response.text(),
  status: response.status,
  headers: response.headers,
  res: response
}

And throws a custom error on 4XX-5XX responses.

But that would be a breaking change.

6TELOIV avatar Nov 18 '25 16:11 6TELOIV

Thanks ! Managed to do a little work around using this for now.

import { type Config } from '@/payload-types'
import { PayloadSDK } from '@payloadcms/sdk'

const originalFetch = typeof window !== 'undefined' ? window.fetch.bind(window) : globalThis.fetch

const wrappedFetch: typeof fetch = async (...args) => {
  const response = await originalFetch(...args)

  if (!response.ok) {
    const errorData = await response.json().catch(() => response)
    throw errorData
  }

  return response
}

export const sdk = new PayloadSDK<Config>({
  baseURL: process.env.NEXT_PUBLIC_SERVER_URL + '/api',
  baseInit: {
    credentials: 'include',
  },
  fetch: wrappedFetch,
})

export default sdk

Siebe-Studio avatar Nov 18 '25 20:11 Siebe-Studio