Errors
The Picked API returns an error in the standard GraphQL format. This guide covers the shape of an error response, every error code, and what your code must do with each one.
Error response shape
When an operation fails, the API returns an errors array in the response body. Each
error object holds these fields:
{
"data": null,
"errors": [
{
"message": "A human-readable description of the error",
"path": ["mutationName"],
"extensions": {
"code": "ERROR_CODE"
}
}
]
}| Field | Description |
|---|---|
message | A description of the error that a person can read. Put it in your log. Do not write code against it. |
path | The path to the field in the query that made the error. |
extensions.code | An error code that a machine can read. Write your code against this field. |
One response can hold more than one error. The data field is null when the whole
operation failed. It holds part of the data when only some fields failed.
Authentication error codes
These errors come from a session, an account, OAuth, a magic link, a password
operation, or MFA. They match the AuthErrorCode enum in the schema.
| Code | Description |
|---|---|
INVALID_CREDENTIALS | The email address and the password do not match. |
EMAIL_NOT_CONFIRMED | The account is there, but nobody verified the email address. See Authentication. |
USER_NOT_FOUND | There is no account with that email address. |
USER_ALREADY_EXISTS | An account with that email address is there already. The API returns this when you create an account. |
WEAK_PASSWORD | The password is not strong enough. |
INVALID_TOKEN | The token is invalid or malformed. This covers a verification token, a reset token, and a magic link token. |
TOKEN_EXPIRED | The token expired. Ask for a new one. |
MFA_CHALLENGE_FAILED | The MFA verification code is not correct. |
MFA_FACTOR_NOT_FOUND | That MFA factor is not there, or it belongs to another caller. |
MFA_REQUIRED | The user must verify a code from an authenticator app before this operation. Complete the challenge, then use the returned session. |
OAUTH_PROVIDER_ERROR | The exchange with the OAuth provider, such as Google, failed. |
RATE_LIMITED | Too many requests in a short period. Wait, then try again. |
Example: invalid credentials
{
"data": {
"sessionCreate": {
"session": null
}
},
"errors": [
{
"message": "Invalid credentials",
"path": ["sessionCreate"],
"extensions": {
"code": "INVALID_CREDENTIALS"
}
}
]
}Authorization error codes
An authorization error happens when the caller does not hold the scope that an operation needs.
| Code | Description |
|---|---|
UNAUTHORIZED | The request carries no valid authentication. It has no Authorization header and no x-api-key header, or the token is invalid. |
FORBIDDEN | The caller is authenticated, but it does not hold the scope that the operation needs. |
Example: the scope is not enough
A caller that holds organization/read only tries to delete an organization. It
receives this:
{
"data": {
"organizationDelete": null
},
"errors": [
{
"message": "Insufficient permissions",
"path": ["organizationDelete"],
"extensions": {
"code": "FORBIDDEN"
}
}
]
}Organization, member, and invitation error codes
These errors come from an organization, a member operation, or an invitation. They
match the ErrorCode enum in the schema.
Organization errors
| Code | Description |
|---|---|
ORGANIZATION_NOT_FOUND | That organization is not there, or the caller cannot reach it. |
ORGANIZATION_ALREADY_EXISTS | An organization with that name is there already. |
SLUG_ALREADY_EXISTS | Another organization uses that slug already. |
INVALID_SLUG | The slug holds an invalid character. A slug takes lowercase letters, digits, and hyphens only. |
Member errors
| Code | Description |
|---|---|
MEMBER_NOT_FOUND | That user is not a member of the organization. |
MEMBER_ALREADY_EXISTS | The user is a member of the organization already. |
CANNOT_REMOVE_PRIMARY_OWNER | You cannot remove the primary owner of an organization. |
CANNOT_CHANGE_OWN_ROLE | A member cannot change the role of that same member. Another owner must do it. |
Invitation errors
| Code | Description |
|---|---|
INVITATION_NOT_FOUND | That invitation is not there. |
INVITATION_EXPIRED | The invitation expired. Nobody can accept it now. |
INVITATION_ALREADY_ACCEPTED | Somebody accepted the invitation already. |
Project errors
| Code | Description |
|---|---|
PROJECT_NOT_FOUND | That project is not there, or the caller cannot reach it. |
General errors
| Code | Description |
|---|---|
VALIDATION_ERROR | The input failed a validation rule. Read the message field to learn which field is invalid. |
INTERNAL_ERROR | The server met an unexpected error. Try the request again. Contact support when the error continues. |
What your code must do
Look for an error first
Check every response for an errors array before you read data:
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify({ query, variables }),
});
const result = await response.json();
if (result.errors) {
for (const error of result.errors) {
const code = error.extensions?.code;
switch (code) {
case 'UNAUTHORIZED':
// Send the user to the sign-in page.
break;
case 'FORBIDDEN':
// Show a permissions error.
break;
case 'RATE_LIMITED':
// Wait, then try again.
break;
default:
// Put the message in the log, or show it.
console.error(error.message);
}
}
}Refresh a token that expired
A request can return UNAUTHORIZED while the caller holds a refresh token. Try a
session refresh first. Do not treat it as a
failure to sign in:
if (code === 'UNAUTHORIZED' && refreshToken) {
const refreshResult = await sessionRefresh(refreshToken);
if (refreshResult.session) {
// Try the first request again with the new access token.
} else {
// Send the user to the sign-in page.
}
}Read data that is only part complete
GraphQL can return part of the data together with an error. Take a query that reads
an organization and its members. It can succeed for the organization and fail for the
members, when the caller does not hold organization_members/read. The response then
holds data with the organization, and errors for the members field. Always read
both.
Try again after a transient failure
INTERNAL_ERROR and RATE_LIMITED are both transient. For RATE_LIMITED, use an
exponential backoff before you try again. For INTERNAL_ERROR, one more try is
usually enough.
