Skip to main content
Skip to main content
Guide

Error Handling

Learn how to handle API errors gracefully with proper retry strategies, error code mappings, and best practices.

Error Codes Reference

CodeName
0
Success
1
ERROR_KEY_DOES_NOT_EXIST
2
ERROR_ZERO_BALANCE
3
ERROR_INVALID_TASK
10
ERROR_CAPTCHA_UNSOLVABLE
12
ERROR_TASK_NOT_FOUND
21
ERROR_TASK_NOT_READY
99
ERROR_INTERNAL

Retry Strategies

Not all errors should be retried. Here is a guide on which errors are retryable and the recommended strategy for each.

Error CodeRetryableStrategy
0
No
Success - no retry needed
1
No
Check API key, do not retry
2
No
Purchase credits first
3
No
Fix request parameters
10
Yes
Retry after 5-10 seconds, max 3 attempts
12
No
Create a new task
21
Yes
Continue polling every 2-3 seconds
99
Yes
Exponential backoff, max 5 attempts
Robust error handling with retry logic
async function solveWithRetry(websiteURL, websiteKey, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      // Create task
      const createRes = await fetch('/api/solver/createTask', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          clientKey: API_KEY,
          task: { type: 'TurnstileTask', websiteURL, websiteKey }
        })
      });
      const createData = await createRes.json();

      // Check for non-retryable errors
      if ([1, 2, 3].includes(createData.errorId)) {
        throw new Error(`Fatal error: ${createData.errorCode}`);
      }
      if (createData.errorId !== 0) {
        throw new Error(`Error: ${createData.errorCode}`);
      }

      // Poll for result with timeout
      const deadline = Date.now() + 60000; // 60s timeout
      while (Date.now() < deadline) {
        await new Promise(r => setTimeout(r, 2500));

        const resultRes = await fetch('/api/solver/getTaskResult', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ clientKey: API_KEY, taskId: createData.taskId })
        });
        const result = await resultRes.json();

        if (result.status === 'ready') return result.solution.token;
        if (result.status === 'failed') throw new Error('Solve failed');
      }

      throw new Error('Timeout waiting for result');
    } catch (error) {
      if (attempt === maxRetries) throw error;
      // Exponential backoff before retry
      await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
    }
  }
}

Common Mistakes

Sending clientKey in headers instead of body

The clientKey must be in the JSON request body, not in HTTP headers.

// Wrong
fetch('/api/solver/createTask', {
  headers: { 'Authorization': 'Bearer cap_...' }
})

// Correct
fetch('/api/solver/createTask', {
  body: JSON.stringify({ clientKey: 'cap_...' })
})

Polling too frequently

Wait at least 2 seconds between getTaskResult calls. Excessive polling may trigger rate limiting.

// Wrong - no delay
while (true) {
  const result = await getTaskResult(taskId);
  if (result.status === 'ready') break;
}

// Correct - 2 second delay
while (true) {
  await new Promise(r => setTimeout(r, 2000));
  const result = await getTaskResult(taskId);
  if (result.status === 'ready') break;
}

Not handling ERROR_TASK_NOT_READY (code 21)

Error code 21 is not a failure -- it means the task is still processing. Continue polling.

// Wrong - treating code 21 as an error
if (result.errorId !== 0) throw new Error('Failed');

// Correct - checking for processing state
if (result.errorId === 21) continue; // Still processing
if (result.errorId !== 0) throw new Error('Failed');

Not storing the taskId before polling

Always save the taskId from createTask before starting to poll for results.

// Wrong - losing the taskId
fetch('/api/solver/createTask', { ... }); // taskId is lost!

// Correct - saving the taskId
const { taskId } = await createResponse.json();
// Now use taskId to poll for results