Error Handling

Unlike REST APIs that use HTTP status codes to signal errors, the Worksome GraphQL API returns HTTP 200 for almost every request, with errors reported inside the response body in the errors array. The exceptions are HTTP 400 (e.g. when the request shape is wrong, such as missing the Content-Type: application/json header). Your client code must always inspect the response payload rather than relying on status codes alone.

The Worksome GraphQL endpoint is fronted by an Apollo Federation gateway in front of the Lighthouse-PHP platform subgraph. Errors fall into two layers: those raised by the gateway itself (parsing, validation, transport) and those forwarded from the platform (auth, business validation, downstream failures). Both layers follow the same GraphQL error specification and add an extensions object that carries the error code and additional metadata.

Error response format

Every error in the response follows the standard GraphQL error structure:

{
  "errors": [
    {
      "message": "Human-readable error description",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": ["mutationName"],
      "extensions": {
        "code": "DOWNSTREAM_SERVICE_ERROR",
        "serviceName": "platform"
      }
    }
  ],
  "data": null
}
Field Type Description
message String A human-readable description of the error.
locations [Location] The line and column in the query where the error originated.
path [String | Int] The path to the field in the response that caused the error.
extensions Object Structured metadata about the error. Always contains code. May also contain serviceName, a validation map, and guards.

Tip

Use extensions.code to determine the type of error programmatically. Do not parse the message string — wording may change without notice.

Error codes

The extensions.code field tells you which broad failure mode the error falls into. Each code maps to a class of problems with a distinct handling strategy.

Code Source Retryable Description
GRAPHQL_VALIDATION_FAILED Gateway No The query references a field, argument, or type that does not exist on the schema.
GRAPHQL_PARSE_FAILED Gateway No The GraphQL document could not be parsed (syntax error).
BAD_REQUEST Gateway No The HTTP request itself is malformed (missing Content-Type, invalid JSON, etc.).
DOWNSTREAM_SERVICE_ERROR Platform via gateway Sometimes The query is syntactically valid but the platform rejected the operation. Discriminate further using extensions.validation, extensions.guards, and the operation path.

DOWNSTREAM_SERVICE_ERROR covers most of what you would think of as “auth” and “business logic” failures, so the most useful work happens inside that bucket.

Schema validation errors

When the query references something that does not exist on the schema, the gateway returns GRAPHQL_VALIDATION_FAILED before the platform even sees the request. This is the most common error to encounter while developing.

# docs:validate-ignore — this example is intentionally invalid
query {
  hires(first: 10) {
    data {
      id
      nonExistentField
    }
  }
}
{
  "errors": [
    {
      "message": "Cannot query field \"nonExistentField\" on type \"Hire\".",
      "locations": [
        {
          "line": 5,
          "column": 7
        }
      ],
      "extensions": {
        "code": "GRAPHQL_VALIDATION_FAILED"
      }
    }
  ]
}

Common causes:

  • Typos in field or type names.
  • Querying a field that was removed or renamed in a schema update.
  • Missing required arguments on a field.
  • Wrong argument types (e.g., passing a String where [Currency!] is required).

Tip

Validate queries against the published schema during development — either with __schema/__type introspection or by exploring the Reference. Catching this class of error before deploy means it never appears in production.

Bad request errors

If the HTTP request itself is malformed (missing Content-Type, invalid JSON body, GET without an apollo-require-preflight header), the gateway returns HTTP 400 with code: "BAD_REQUEST" before any GraphQL processing.

{
  "errors": [
    {
      "message": "This operation has been blocked as a potential Cross-Site Request Forgery (CSRF). Please either specify a 'content-type' header (with a type that is not one of application/x-www-form-urlencoded, multipart/form-data, text/plain) or provide a non-empty value for one of the following headers: x-apollo-operation-name, apollo-require-preflight",
      "extensions": {
        "code": "BAD_REQUEST"
      }
    }
  ]
}

Make sure every request includes:

  • Authorization: Bearer {token}
  • Content-Type: application/json
  • A POST body containing { "query": "...", "variables": { ... } }.

Validation errors

Input that fails the platform’s business rules surfaces as DOWNSTREAM_SERVICE_ERROR with an extensions.validation map of dot-notation field paths to error messages. The presence of validation is the signal that this error came from the input layer rather than from auth or business logic.

mutation {
  createDraftHire(input: {
    company: "Q29tcGFueTox"
    name: ""
    startDate: "not-a-date"
  }) {
    id
  }
}
{
  "errors": [
    {
      "message": "Validation failed for the field [createDraftHire].",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": ["createDraftHire"],
      "extensions": {
        "validation": {
          "input.name": ["The name field is required."],
          "input.startDate": ["The start date is not a valid date."]
        },
        "serviceName": "platform",
        "code": "DOWNSTREAM_SERVICE_ERROR"
      }
    }
  ],
  "data": {
    "createDraftHire": null
  }
}

The keys in extensions.validation correspond to the input field paths using dot notation (e.g., input.name). Each key maps to an array of error messages for that field.

Tip

Use extensions.validation to display inline field-level errors in your UI. Map the dot-notation keys back to your form fields for a smooth user experience.

Authentication errors

A missing or invalid bearer token comes back as DOWNSTREAM_SERVICE_ERROR with guards: ["api"] in extensions. This is the GraphQL equivalent of an HTTP 401.

{
  "errors": [
    {
      "message": "Unauthenticated.",
      "extensions": {
        "guards": ["api"],
        "serviceName": "platform",
        "code": "DOWNSTREAM_SERVICE_ERROR"
      }
    }
  ],
  "data": null
}

Common causes:

  • The Authorization header is missing or malformed.
  • The API token has expired (Personal Access Tokens are valid for 6 months) or been revoked.
  • The token does not have the required scopes.

Tip

Discriminate this case from generic platform errors by checking for extensions.guards containing "api" plus the message "Unauthenticated.".

Authorization errors

Authorization errors occur when the authenticated user does not have permission to perform the requested operation. They also surface as DOWNSTREAM_SERVICE_ERROR, with no validation map and no guards — the operation path and message indicate which mutation/field was rejected.

mutation {
  terminateHire(input: {
    hire: "SGlyZTox"
    reason: PROJECT_COMPLETED_EARLY
    date: "2026-04-01"
  }) {
    id
  }
}
{
  "errors": [
    {
      "message": "You are not authorized to perform this action.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": ["terminateHire"],
      "extensions": {
        "serviceName": "platform",
        "code": "DOWNSTREAM_SERVICE_ERROR"
      }
    }
  ],
  "data": {
    "terminateHire": null
  }
}

Common causes:

  • The authenticated user does not have the required role on the company that owns the resource.
  • The resource belongs to a different company or workspace.
  • A field-level restriction is in effect (the field returns null in data and an entry shows up in errors — see Partial responses).

Warning

Authorization errors are not retryable with the same credentials. Verify that the API token belongs to a user with the correct role and permissions for the operation.

Server errors

If the platform itself fails (timeout, unhandled exception), the gateway forwards a DOWNSTREAM_SERVICE_ERROR with serviceName: "platform" and a generic message. These are typically transient — wait briefly and retry.

{
  "errors": [
    {
      "message": "Internal server error.",
      "extensions": {
        "serviceName": "platform",
        "code": "DOWNSTREAM_SERVICE_ERROR"
      }
    }
  ],
  "data": null
}

Caution

If a server error persists after multiple retries, contact Worksome support with the full error response and the query you were executing.

Handling errors in code

Here is an example of a robust error-handling pattern in TypeScript. It switches on extensions.code and uses the additional metadata (validation, guards) to discriminate the platform-side codes.

interface GraphQLError {
  message: string;
  locations?: { line: number; column: number }[];
  path?: (string | number)[];
  extensions?: {
    code?: string;
    serviceName?: string;
    guards?: string[];
    validation?: Record<string, string[]>;
  };
}

interface GraphQLResponse<T> {
  data: T | null;
  errors?: GraphQLError[];
}

async function executeQuery<T>(query: string, variables?: Record<string, unknown>): Promise<T> {
  const response = await fetch("https://api.worksome.com/graphql", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${API_TOKEN}`,
    },
    body: JSON.stringify({ query, variables }),
  });

  const result: GraphQLResponse<T> = await response.json();

  if (result.errors && result.errors.length > 0) {
    for (const error of result.errors) {
      const code = error.extensions?.code;

      switch (code) {
        case "GRAPHQL_VALIDATION_FAILED":
        case "GRAPHQL_PARSE_FAILED":
          // Query references something that does not exist on the schema.
          // This is a developer error — fix the query.
          console.error("GraphQL query error:", error.message, error.locations);
          throw new QueryError(error.message);

        case "BAD_REQUEST":
          // The HTTP request itself was malformed.
          throw new BadRequestError(error.message);

        case "DOWNSTREAM_SERVICE_ERROR":
          // Discriminate using the additional extensions fields.
          if (error.extensions?.validation) {
            throw new ValidationError(error.message, error.extensions.validation);
          }
          if (error.extensions?.guards?.includes("api") && error.message === "Unauthenticated.") {
            throw new AuthenticationError(error.message);
          }
          // Otherwise it is an authorization or business-rule failure.
          throw new PlatformError(error.message, error.path);

        default:
          throw new Error(error.message);
      }
    }
  }

  return result.data as T;
}

A corresponding validation error class that makes field errors convenient to work with:

class ValidationError extends Error {
  fieldErrors: Record<string, string[]>;

  constructor(message: string, fieldErrors: Record<string, string[]>) {
    super(message);
    this.fieldErrors = fieldErrors;
  }

  /**
   * Get errors for a specific input field.
   * Pass the full dot-notation path, e.g. "input.name".
   */
  getFieldErrors(field: string): string[] {
    return this.fieldErrors[field] ?? [];
  }

  /**
   * Check whether a specific field has validation errors.
   */
  hasFieldError(field: string): boolean {
    return this.getFieldErrors(field).length > 0;
  }
}

Partial responses

GraphQL can return both data and errors in the same response. This happens when some fields resolve successfully while others fail. The data object will contain null for the fields that errored, with corresponding entries in the errors array.

query {
  hire(id: "SGlyZTox") {
    id
    activeStatus
    rate
    worker {
      name
      email
    }
  }
}
{
  "errors": [
    {
      "message": "You are not authorized to access this field.",
      "path": ["hire", "rate"],
      "extensions": {
        "serviceName": "platform",
        "code": "DOWNSTREAM_SERVICE_ERROR"
      }
    }
  ],
  "data": {
    "hire": {
      "id": "SGlyZTox",
      "activeStatus": "ACTIVE",
      "rate": null,
      "worker": {
        "name": "Jane Doe",
        "email": "[email protected]"
      }
    }
  }
}

In this example, the query succeeded overall and returned the hire, worker name, and email, but the authenticated user did not have permission to view the rate. The rate field is null and the reason is explained in the errors array.

Warning

Always check the errors array even when data is present. A non-null data object does not guarantee that the entire query succeeded.

Retry strategy

Not all errors are retryable. Use the error code (and the discriminators inside DOWNSTREAM_SERVICE_ERROR) to decide whether to retry a failed request.

Failure mode Retryable Action
GRAPHQL_VALIDATION_FAILED / GRAPHQL_PARSE_FAILED No Fix the query. This is a developer error.
BAD_REQUEST No Fix the request shape (headers, body).
DOWNSTREAM_SERVICE_ERROR with validation map No Fix the input and resubmit.
DOWNSTREAM_SERVICE_ERROR with guards: ["api"] (Unauthenticated.) No Refresh or regenerate your API token.
DOWNSTREAM_SERVICE_ERROR (authorization) No Verify user permissions. Do not retry with the same credentials.
DOWNSTREAM_SERVICE_ERROR (transient platform error) Yes Retry with exponential backoff.

For retryable errors, use exponential backoff with jitter to avoid thundering-herd problems:

async function executeWithRetry<T>(
  query: string,
  variables?: Record<string, unknown>,
  maxRetries: number = 3,
): Promise<T> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await executeQuery<T>(query, variables);
    } catch (error) {
      if (error instanceof PlatformError && attempt < maxRetries) {
        const baseDelay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
        const jitter = Math.random() * 1000;
        await new Promise((resolve) => setTimeout(resolve, baseDelay + jitter));
        continue;
      }
      throw error;
    }
  }

  throw new Error("Max retries exceeded");
}

Caution

Never retry validation, authentication, or authorization errors automatically. They will fail with the same result every time and may trigger rate limiting.

Best practices

Always check the errors array

Every response from the Worksome GraphQL API may contain an errors array. Check it on every request, even when data is present.

Handle partial responses gracefully

When you receive both data and errors, use the data that resolved successfully and handle the errors for the fields that failed. Do not discard the entire response because of a single field error.

Switch on extensions.code, not on message

Route your error-handling logic based on extensions.code (and the secondary discriminators within DOWNSTREAM_SERVICE_ERROR) rather than parsing error messages. Codes are stable; messages may be rephrased.

Log error details for debugging

Include the full error object — message, path, locations, and extensions — in your application logs. The path field is especially useful for pinpointing which part of a complex query failed.

Display user-friendly messages

The message field is intended as a developer-facing description. Map error codes to user-friendly messages in your UI rather than showing raw API errors to end users. The exception is validation errors, where extensions.validation messages are suitable for display.

Validate queries during development

Use schema introspection (__schema/__type) and the Reference to validate your queries before deploying them. Catching GRAPHQL_VALIDATION_FAILED errors locally means they never reach production.