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.
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"]
}
]
}{
"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"]
}
]
}{
"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 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)"
}
]
}{
"errors": [
{
"message": "Missing required scopes: (usertests:write)"
}
]
}{
"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.
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."
}
]
}{
"errors": [
{
"message": "Variable '$input' of required type 'CreateUserTestInput!' was not provided."
}
]
}{
"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.
In rare cases, an unexpected server error may occur:
{
"errors": [
{
"message": "Unhandled server error"
}
]
}{
"errors": [
{
"message": "Unhandled server error"
}
]
}{
"errors": [
{
"message": "Unhandled server error"
}
]
}Resolution: If you consistently encounter this error, contact our support team with the full request details and timestamp.
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."
}
}
}
}{
"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."
}
}
}
}{
"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
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();
if (errors) {
handleGraphQLErrors(errors);
return;
}
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 {
console.log('Created user test:', userTest);
redirectToTest(userTest.uuid);
}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();
if (errors) {
handleGraphQLErrors(errors);
return;
}
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 {
console.log('Created user test:', userTest);
redirectToTest(userTest.uuid);
}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();
if (errors) {
handleGraphQLErrors(errors);
return;
}
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 {
console.log('Created user test:', userTest);
redirectToTest(userTest.uuid);
}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
}
}
}{
"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
}
}
}{
"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}`);
});
}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}`);
});
}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
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
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
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
}
}
}
}
}query GetUserTest($pk: Int!) {
userTest(pk: $pk) {
pk
uuid
status
results(first: 10) {
edges {
node {
pk
responseUUID
submittedAt
}
}
}
}
}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"]
}
]
}{
"data": {
"userTest": null
},
"errors": [
{
"message": "You don't have access to that user test",
"path": ["userTest"]
}
]
}{
"data": {
"userTest": null
},
"errors": [
{
"message": "You don't have access to that user test",
"path": ["userTest"]
}
]
}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);
if (result.errors) {
return handleGraphQLErrors(result.errors);
}
const mutationResult = result.data.createUserTest;
if (!mutationResult.ok) {
return handleOperationError(mutationResult.error);
}
return mutationResult.userTest;const result = await executeGraphQL(mutation, variables);
if (result.errors) {
return handleGraphQLErrors(result.errors);
}
const mutationResult = result.data.createUserTest;
if (!mutationResult.ok) {
return handleOperationError(mutationResult.error);
}
return mutationResult.userTest;const result = await executeGraphQL(mutation, variables);
if (result.errors) {
return handleGraphQLErrors(result.errors);
}
const mutationResult = result.data.createUserTest;
if (!mutationResult.ok) {
return handleOperationError(mutationResult.error);
}
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:
if (error.code === 'QUOTA_REACHED') {
showUpgradeModal();
}
if (error.message.includes('maxed out')) {
showUpgradeModal();
}
if (error.code === 'QUOTA_REACHED') {
showUpgradeModal();
}
if (error.message.includes('maxed out')) {
showUpgradeModal();
}
if (error.code === 'QUOTA_REACHED') {
showUpgradeModal();
}
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');const contextualMessage = `Failed to create user test "${testName}": ${error.message}`;
showNotification(contextualMessage, 'error');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()
});console.error('Mutation failed:', {
operation: 'createUserTest',
error: error,
variables: variables,
timestamp: new Date().toISOString()
});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:
if (errors?.some(e => e.message.includes('Missing required scopes'))) {
window.location.href = '/auth/reauthorize';
return;
}
if (errors?.some(e => e.message.includes('OAuth2 token expired'))) {
await refreshAccessToken();
return retryRequest();
}
if (errors?.some(e => e.message.includes('Missing required scopes'))) {
window.location.href = '/auth/reauthorize';
return;
}
if (errors?.some(e => e.message.includes('OAuth2 token expired'))) {
await refreshAccessToken();
return retryRequest();
}
if (errors?.some(e => e.message.includes('Missing required scopes'))) {
window.location.href = '/auth/reauthorize';
return;
}
if (errors?.some(e => e.message.includes('OAuth2 token expired'))) {
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') }
]
});
}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') }
]
});
}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') }
]
});
}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
}
}
}query GetCreateUserTestErrorCodes {
__type(name: "CreateUserTestErrorEnum") {
name
enumValues {
name
description
}
}
}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."
}
]
}
}
}{
"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."
}
]
}
}
}{
"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."
}
]
}
}
}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.)