Black-and-white line illustration of a warning triangle and tools for debugging and fixes.
Black-and-white line illustration of a warning triangle and tools for debugging and fixes.

Errors and troubleshooting

GraphQL error codes, null fields and diagnostic practices

Understanding error handling in the Ballpark GraphQL API

The Ballpark API employs a dual-layer error system that provides both GraphQL-level errors and operation-specific errors. This design ensures you receive detailed, actionable information when something goes wrong, while maintaining the flexibility and type safety that GraphQL provides.

Error response structure

A typical GraphQL response from the Ballpark API may contain errors in two distinct locations:

{
  "data": {
    "createUserTest": {
      "ok": false,
      "userTest": null,
      "error": {
        "code": "PROJECT_DOES_NOT_EXIST",
        "message": "The project you're trying to test does not exist"
      }
    }
  },
  "errors": [
    {
      "message": "Missing required scopes: (usertests:write)",
      "locations": [{"line": 2, "column": 3}],
      "path": ["createUserTest"]
    }
  ]
}

GraphQL-level errors

GraphQL-level errors appear in the top-level errors array and indicate issues with the request itself, authentication, or server-level problems. These errors prevent the operation from executing entirely.

Authentication and scope errors

When your access token is missing, expired, or lacks the required scopes, you'll receive an error in the errors array:

{
  "errors": [
    {
      "message": "Missing required scopes: (usertests:write)"
    }
  ]
}

Common authentication errors:

  • Missing required scopes: (scope:name) — Your token lacks the necessary OAuth scope for this operation

  • OAuth2 token expired or invalid — Your access token has expired or is malformed

Resolution: Ensure your access token includes all required scopes for the operation you're attempting. Refer to our Authentication guide for obtaining properly scoped tokens.

Validation errors

Input validation failures occur when the structure or content of your request doesn't meet the API's requirements:

{
  "errors": [
    {
      "message": "Variable '$input' of required type 'CreateUserTestInput!' was not provided."
    }
  ]
}

Resolution: Verify that all required fields are present and that data types match the schema definition. Use introspection or our schema documentation to validate your query structure.

Server errors

In rare cases, an unexpected server error may occur:

{
  "errors": [
    {
      "message": "Unhandled server error"
    }
  ]
}

Resolution: If you consistently encounter this error, contact our support team with the full request details and timestamp.

Operation-specific errors

When an operation executes successfully but encounters a business logic constraint, the error appears within the mutation's response data. These errors are operation-specific and include a typed error code alongside a descriptive message.

Standard error structure

Most mutations follow this response pattern:

{
  "data": {
    "createUserTest": {
      "ok": false,
      "userTest": null,
      "error": {
        "code": "QUOTA_REACHED",
        "message": "Your account has maxed out the number of allowed user tests. Upgrade, or archive some and try again."
      }
    }
  }
}

Key fields:

  • ok (Boolean) — Indicates whether the operation succeeded

  • userTest (Object or null) — The created resource on success, null on failure

  • error (Object or null) — Contains error details if ok is false

    • code (Enum) — A machine-readable error code

    • message (String) — A human-readable error description

Using error codes

Error codes are enumerated values that allow your application to handle specific error conditions programmatically:

const response = await fetch(GRAPHQL_ENDPOINT, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${accessToken}`
  },
  body: JSON.stringify({
    query: `
      mutation CreateUserTest($input: CreateUserTestInput!) {
        createUserTest(input: $input) {
          ok
          userTest {
            pk
            uuid
            name
          }
          error {
            code
            message
          }
        }
      }
    `,
    variables: {
      input: {
        projectPk: 12345,
        name: "Homepage Usability Test"
      }
    }
  })
});

const { data, errors } = await response.json();

// Check for GraphQL-level errors first
if (errors) {
  handleGraphQLErrors(errors);
  return;
}

// Check for operation-specific errors
const { ok, error, userTest } = data.createUserTest;

if (!ok && error) {
  switch (error.code) {
    case 'PERMISSION_DENIED':
      showPermissionError();
      break;
    case 'PROJECT_DOES_NOT_EXIST':
      showProjectNotFoundError();
      break;
    case 'PROJECT_IS_ARCHIVED':
      showArchivedProjectError();
      break;
    case 'QUOTA_REACHED':
      showUpgradePrompt();
      break;
    case 'MISSING_ARGUMENTS':
      showValidationError(error.message);
      break;
    default:
      showGenericError(error.message);
  }
} else {
  // Success - user test was created
  console.log('Created user test:', userTest);
  redirectToTest(userTest.uuid);
}

Common error codes

While each mutation defines its own error codes, several patterns recur throughout the API:

Permission and access

  • PERMISSION_DENIED — Your account lacks permission to perform this action

  • UNAUTHORIZED — Authentication is required or credentials are invalid

Resource management

  • PROJECT_DOES_NOT_EXIST / PROJECT_NOT_FOUND — The requested project doesn't exist

  • SCREEN_NOT_FOUND — The requested screen doesn't exist

  • INVALID_TARGET_FOLDER — The specified folder doesn't exist

  • UNAUTHORIZED_TARGET_FOLDER — You don't have access to the specified folder

Validation

  • VALIDATION_ERROR — Input data failed validation rules

  • INVALID_INPUT — Input structure or content is malformed

  • MISSING_ARGUMENTS — Required parameters were not provided

  • INVALID_GOAL_SCREEN — The goal screen specified is not valid

  • BROKEN_SCREENS — The test project contains broken screens that must be fixed

Quota and limits

  • QUOTA_REACHED — Account has reached its usage limit for this resource

State conflicts

  • PROJECT_IS_ARCHIVED — Cannot perform this operation on an archived project

  • READONLY_PROJECT — The project is read-only and cannot be modified

Integration errors

  • EXTERNAL_PROTOTYPE_IMPORT_FAILED — Failed to import the remote project

  • EXTERNAL_PROTOTYPE_UNAUTHORISED_ACCESSS — You lack permission to access the remote project

  • EXTERNAL_PROTOTYPE_NOT_FOUND — The remote file could not be found

Batch operations and partial failures

Some mutations operate on multiple items simultaneously. These return succeeded and failed arrays to indicate which items were processed successfully:

{
  "data": {
    "createScreens": {
      "succeeded": [
        {
          "pk": 101,
          "name": "Home",
          "displayName": "Home"
        },
        {
          "pk": 102,
          "name": "About",
          "displayName": "About"
        }
      ],
      "failed": [
        {
          "name": "Contact",
          "code": "MULTIPLE_NAMES",
          "message": "Multiple images with this name found"
        }
      ],
      "error": null
    }
  }
}

This pattern allows your application to handle partial success gracefully—you can confirm which operations completed while addressing any failures:

const { succeeded, failed } = data.createScreens;

if (succeeded.length > 0) {
  console.log(`Successfully created ${succeeded.length} screens`);
  succeeded.forEach(screen => {
    console.log(`- ${screen.name} (pk: ${screen.pk})`);
  });
}

if (failed.length > 0) {
  console.warn(`Failed to create ${failed.length} screens`);
  failed.forEach(failure => {
    console.warn(`- ${failure.name}: ${failure.message}`);
  });
}

Usability Testing Steps

  • STEP_PROTOTYPE_TASK — Tests a specific task within a prototype

  • STEP_WEBSITE_TASK — Tests a task performed on a live website

  • STEP_FIRST_CLICK — Tests what a participant is likely to click first on a screen

  • STEP_FIVE_SECOND_TEST — Checks a participant's response after viewing an image for five seconds

Marketing and Design Steps

  • STEP_PREFERENCE_TEST — Allows participants to select their preference from a set of options

  • STEP_BANNER_AD_TEST — Tests the effectiveness of banner ad copy

  • STEP_TAGLINE_COPY_TEST — Tests the effectiveness of tagline or copy text

Card Sorting Steps

  • STEP_CARDSORT_OPEN — Participants sort cards into categories they create themselves

  • STEP_CARDSORT_CLOSED — Participants sort cards into predefined categories

  • STEP_CARDSORT_HYBRID — Participants sort cards into predefined categories or create new ones

AI-Powered Steps

  • STEP_AI_INTERVIEW — An AI-moderated interview where the AI asks dynamic follow-up questions based on participant responses

Queries can also return errors when resources don't exist or you lack permission to access them:

query GetUserTest($pk: Int!) {
  userTest(pk: $pk) {
    pk
    uuid
    status
    results(first: 10) {
      edges {
        node {
          pk
          responseUUID
          submittedAt
        }
      }
    }
  }
}

If the user test doesn't exist or you lack access, you'll receive a GraphQL-level error:

{
  "data": {
    "userTest": null
  },
  "errors": [
    {
      "message": "You don't have access to that user test",
      "path": ["userTest"]
    }
  ]
}

Best practices

1. Always check both error locations

Your error handling should account for both GraphQL-level errors and operation-specific errors:

const result = await executeGraphQL(mutation, variables);

// Check GraphQL-level errors first
if (result.errors) {
  return handleGraphQLErrors(result.errors);
}

// Check operation-specific errors
const mutationResult = result.data.createUserTest;
if (!mutationResult.ok) {
  return handleOperationError(mutationResult.error);
}

// Success
return mutationResult.userTest;

2. Use error codes for conditional logic

Rely on error code values rather than parsing error messages, as codes are stable across API versions:

// Good
if (error.code === 'QUOTA_REACHED') {
  showUpgradeModal();
}

// Avoid
if (error.message.includes('maxed out')) {
  showUpgradeModal();
}

3. Provide context in error messages

When displaying errors to users, combine the error message with operation context:

const contextualMessage = `Failed to create user test "${testName}": ${error.message}`;
showNotification(contextualMessage, 'error');

4. Log full error details

For debugging purposes, log complete error objects including any additional context fields:

console.error('Mutation failed:', {
  operation: 'createUserTest',
  error: error,
  variables: variables,
  timestamp: new Date().toISOString()
});

5. Handle authentication errors globally

Authentication and scope errors typically require the same response regardless of operation. Consider handling these at a global level in your application:

// In your GraphQL client wrapper
if (errors?.some(e => e.message.includes('Missing required scopes'))) {
  // Redirect to obtain proper token
  window.location.href = '/auth/reauthorize';
  return;
}

if (errors?.some(e => e.message.includes('OAuth2 token expired'))) {
  // Refresh token
  await refreshAccessToken();
  return retryRequest();
}

6. Handle quota errors gracefully

When users hit quota limits, provide clear upgrade paths:

if (error.code === 'QUOTA_REACHED') {
  showModal({
    title: 'User Test Limit Reached',
    message: error.message,
    actions: [
      { label: 'View Plans', onClick: () => navigateTo('/pricing') },
      { label: 'Archive Tests', onClick: () => navigateTo('/tests/archive') }
    ]
  });
}

Introspecting error types

The Ballpark GraphQL schema includes full type definitions for all error objects. You can use introspection to discover available error codes for any mutation:

query GetCreateUserTestErrorCodes {
  __type(name: "CreateUserTestErrorEnum") {
    name
    enumValues {
      name
      description
    }
  }
}

This returns all possible error codes and their descriptions:

{
  "data": {
    "__type": {
      "name": "CreateUserTestErrorEnum",
      "enumValues": [
        {
          "name": "PROJECT_DOES_NOT_EXIST",
          "description": "The project you're trying to test does not exist"
        },
        {
          "name": "PROJECT_IS_ARCHIVED",
          "description": "The project you're trying to test is archived"
        },
        {
          "name": "PERMISSION_DENIED",
          "description": "You do not have sufficient permissions to access the project you're trying to test"
        },
        {
          "name": "QUOTA_REACHED",
          "description": "Your account has maxed out the number of allowed projects. Archive some to continue."
        },
        {
          "name": "MISSING_ARGUMENTS",
          "description": "One or more required arguments are missing from your call"
        },
        {
          "name": "INVALID_GOAL_SCREEN",
          "description": "The goalScreenPk you've supplied is not valid"
        },
        {
          "name": "BROKEN_SCREENS",
          "description": "The test project contains broken screens. Fix or remove those before retrying."
        }
      ]
    }
  }
}

Further assistance

If you encounter errors that aren't covered in this guide, or if you believe you've found an issue with the API, please contact us with:

  • The full GraphQL query or mutation

  • The complete error response

  • Your request timestamp

  • Any relevant identifiers (project PK, user test UUID, etc.)