GraphQL APIs return HTTP 200 even when something goes wrong. Always check the errors array.
GraphQL APIs handle errors differently from REST APIs, and the difference matters: you can't rely on HTTP status codes alone to detect failure. Always inspect the response body.
HTTP status codes
| Code | Meaning |
|---|---|
200 OK | The request reached the API and was processed. The operation may still have failed — check the response body. |
400 Bad Request | The request body wasn't valid JSON, or the GraphQL document was unparseable. |
401 Unauthorized | The Authorization header is missing, malformed, or the token is expired or signed by an unknown issuer. |
403 Forbidden | The token is structurally valid but rejected before reaching a resolver — for example, the token's audience doesn't match the API. |
429 Too Many Requests | You've exceeded a rate limit. Back off and retry. |
5xx | Server-side error. Retry with exponential backoff. |
The most important thing to internalize: 200 OK does not mean your operation succeeded. GraphQL returns 200 even when:
- The operation hit an authorization check inside a resolver and was denied.
- The operation tried to update a record that didn't exist.
- A required field was null in the input.
- The resolver threw an exception.
In all of these cases, the HTTP status is 200 and the failure information is in the response body's errors array.
Response body shape
Every successful HTTP response (200) follows this shape:
{
"data": { /* operation result, or null on hard failure */ },
"errors": [ /* present when something went wrong; absent on full success */ ]
}Three valid combinations:
// Full success
{ "data": { "getDataset": { "id": "abc", "name": "Sales" } } }
// Hard failure — no data returned
{
"data": null,
"errors": [
{ "errorType": "Unauthorized", "message": "Not authorized to access getDataset on type Query" }
]
}
// Partial success — some fields returned, others failed
{
"data": { "getDataset": { "id": "abc", "name": "Sales", "owner": null } },
"errors": [
{ "path": ["getDataset", "owner"], "errorType": "ResolverFailure", "message": "User lookup timed out" }
]
}The third case is GraphQL-specific and worth handling deliberately. A request can be both successful (you got data) and failed (some fields didn't resolve) at the same time. For most non-critical fields this is fine — render what you got. For critical fields, treat any non-empty errors array as a failure.
Common error types
Every error object has an errorType field that classifies the failure. Handle these explicitly:
errorType | What happened | What to do |
|---|---|---|
UnauthorizedException | Token missing, malformed, or expired. | Get a fresh token and retry. |
Unauthorized | Token is valid but your account can't access this resource or operation. | Don't retry — surface the error to the user. |
ValidationError | Your variables don't match the operation's input shape, or a required field was missing. | Fix the request shape; this is a programming error. |
ConditionalCheckFailedException | You tried to update a record using a stale version (optimistic lock). | Refetch the record and retry. |
ResourceNotFoundException | The ID you passed doesn't exist or has been deleted. | Don't retry — surface to the user. |
ThrottlingException | You've hit a rate limit. | Retry with exponential backoff (start at 1s, double up to ~30s, then give up). |
InternalFailure | Something went wrong inside BEEM. | Retry once or twice with backoff; if it persists, contact support. |
Less common types you might see:
BadRequestException— the GraphQL document parsed but referenced fields that don't exist (schema mismatch).PaginationOverflow— you've requested too many items per page; reducelimit.NotImplemented— operation is documented but not yet enabled in your environment.
Handling errors in code
A robust client checks both the HTTP status and the errors array:
async function callBeem(query, variables) {
const res = await fetch('https://api.beemdata.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ query, variables }),
});
if (res.status === 401) {
throw new AuthError('Token expired or invalid');
}
if (res.status === 429) {
throw new RateLimitError('Throttled');
}
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const body = await res.json();
if (body.errors && body.errors.length > 0) {
// Partial successes are still in body.data; decide per use case.
const types = body.errors.map(e => e.errorType);
if (types.includes('UnauthorizedException')) throw new AuthError('Token expired');
if (types.includes('ThrottlingException')) throw new RateLimitError('Throttled');
throw new ApiError(body.errors);
}
return body.data;
}The pattern matters: don't just call res.json() and hand the result to your application. Inspect errors first.
Reporting issues
If you see an error that isn't documented here or behaves unexpectedly, capture the full response body (including the errors array, the path if present, and any errorInfo object) and contact [email protected].
