Error handling

Written By Stanislas

Last updated 16 days ago


Best practices and patterns for handling errors when integrating the Swiftask Public Bot API. Learn how to identify errors, implement recovery strategies, and provide user-friendly error messages.

Overview

Errors can occur at multiple levels: REST authentication failures, GraphQL validation errors, network issues, WebSocket disconnections, and server errors. This guide covers how to identify each error type, implement appropriate recovery strategies, and maintain a good user experience when things go wrong.

Proper error handling is critical for production applications. It ensures your app doesn't crash, users understand what went wrong, and the application can recover automatically when possible.


Prerequisites

Before implementing error handling, ensure you have:

  1. Read the foundational guides β€” Understand Authentication, Chat Operations, and Subscriptions

  2. Apollo Client configured β€” With proper HTTP and WebSocket links

  3. Understanding of async/await β€” Error handling uses try-catch blocks

  4. Basic logging setup β€” For debugging errors in production


Getting started

Here's a minimal error handling setup for the most common errors:

Step 1: Wrap operations in try-catch

Always wrap API calls in try-catch blocks.

async function sendMessageSafely(client, sessionId, message) {
  try {
    const { data } = await client.mutate({
      mutation: SEND_MESSAGE,
      variables: {
        newMessageData: {
          message,
          sessionId,
          isForAiReply: true,
        },
      },
    });

    console.log('Message sent:', data.sendNewMessage.id);
    return data.sendNewMessage;
  } catch (error) {
    console.error('Failed to send message:', error);
    throw error;
  }
}

Step 2: Identify error type

Distinguish between network, GraphQL, and other errors.

function getErrorType(error) {
  if (error.networkError) {
    return 'NETWORK_ERROR';
  }

  if (error.graphQLErrors?.length > 0) {
    return 'GRAPHQL_ERROR';
  }

  if (error instanceof TypeError) {
    return 'TYPE_ERROR';
  }

  return 'UNKNOWN_ERROR';
}

// Usage
try {
  await sendMessage(sessionId, message);
} catch (error) {
  const errorType = getErrorType(error);
  console.log('Error type:', errorType);
}

Step 3: Show user-friendly messages

Map technical errors to user-friendly messages.

const ERROR_MESSAGES = {
  NETWORK_ERROR: 'Unable to connect. Please check your internet connection.',
  AUTH_ERROR: 'Authentication failed. Please refresh the page.',
  SESSION_NOT_FOUND: 'Chat session not found. Please start a new conversation.',
  MESSAGE_SEND_FAILED: 'Failed to send message. Please try again.',
  STREAM_ERROR: 'Connection interrupted. Reconnecting...',
  RATE_LIMIT: 'Too many requests. Please wait a moment.',
  SERVER_ERROR: 'Something went wrong on our end. Please try again later.',
};

function getUserFriendlyError(error) {
  if (error.networkError) {
    return ERROR_MESSAGES.NETWORK_ERROR;
  }

  if (error.graphQLErrors?.length > 0) {
    const code = error.graphQLErrors[0].extensions?.code;
    if (code === 'UNAUTHENTICATED') {
      return ERROR_MESSAGES.AUTH_ERROR;
    }
    if (code === 'NOT_FOUND') {
      return ERROR_MESSAGES.SESSION_NOT_FOUND;
    }
  }

  return ERROR_MESSAGES.SERVER_ERROR;
}

// Usage
try {
  await sendMessage(sessionId, message);
} catch (error) {
  const friendlyMessage = getUserFriendlyError(error);
  showErrorToUser(friendlyMessage);
}HTTP authentication errors

Error codes and causes

Code

Description

Cause

Fix

400

Bad Request

Missing or invalid parameters

Check clientToken and x-client-uuid header

401

Unauthorized

Invalid or expired token

Verify clientToken is correct

403

Forbidden

Agent is not public

Enable public access in agent settings

404

Not Found

Agent doesn't exist

Confirm agent exists in workspace

500

Internal Server Error

Server-side issue

Retry with exponential backoff

Handling authentication errors

async function authenticateWithErrorHandling(clientToken, clientUuid) {
  try {
    const response = await fetch(
      `https://graphql.swiftask.ai/public/widget-bot/${clientToken}`,
      {
        method: 'GET',
        headers: {
          'x-client-uuid': clientUuid,
          'Content-Type': 'application/json',
        },
      }
    );

    if (!response.ok) {
      const error = await response.json();

      switch (response.status) {
        case 400:
          throw new Error('Invalid request: Check your parameters');
        case 401:
          throw new Error('Authentication failed: Invalid client token');
        case 403:
          throw new Error('Access denied: Agent may not be public');
        case 404:
          throw new Error('Agent not found');
        case 500:
          throw new Error('Server error: Try again later');
        default:
          throw new Error(`HTTP ${response.status}: ${error.message}`);
      }
    }

    return await response.json();
  } catch (error) {
    if (error instanceof TypeError) {
      // Network error (DNS, timeout, etc.)
      throw new Error('Network error: Please check your connection');
    }
    throw error;
  }
}

// Usage
try {
  const { data } = await authenticateWithErrorHandling(clientToken, clientUuid);
  console.log('Authenticated successfully');
} catch (error) {
  console.error('Authentication error:', error.message);
  showErrorToUser(error.message);
}

GraphQL errors

Understanding GraphQL error structure

GraphQL errors contain detailed information:

// GraphQL error structure
const graphQLError = {
  message: 'User not found',
  locations: [{ line: 1, column: 8 }],
  path: ['sendNewMessage'],
  extensions: {
    code: 'NOT_FOUND',
    exception: {
      stacktrace: [...],
    },
  },
};

Common GraphQL error codes

Code

Meaning

How to Handle

UNAUTHENTICATED

Invalid or expired token

Re-authenticate user

FORBIDDEN

Access denied

Check permissions, show access denied message

BAD_USER_INPUT

Invalid input data

Validate input, show error to user

INTERNAL_SERVER_ERROR

Server error

Retry with exponential backoff

NOT_FOUND

Resource not found

Verify IDs, show not found message

RATE_LIMITED

Too many requests

Wait and retry

Handling GraphQL errors

import { ApolloError } from '@apollo/client';

async function sendMessageWithErrorHandling(client, sessionId, message) {
  try {
    const { data } = await client.mutate({
      mutation: SEND_MESSAGE,
      variables: {
        newMessageData: {
          message,
          sessionId,
          isForAiReply: true,
        },
      },
    });

    return data.sendNewMessage;
  } catch (error) {
    if (error instanceof ApolloError) {
      // GraphQL errors
      if (error.graphQLErrors?.length > 0) {
        error.graphQLErrors.forEach((err) => {
          const code = err.extensions?.code;
          const message = err.message;

          console.error('GraphQL Error:', {
            code,
            message,
            path: err.path,
          });

          // Handle specific error codes
          switch (code) {
            case 'UNAUTHENTICATED':
              console.error('Token expired, re-authenticating...');
              handleAuthError();
              break;

            case 'FORBIDDEN':
              console.error('User does not have access');
              showErrorToUser('You do not have permission to perform this action');
              break;

            case 'BAD_USER_INPUT':
              console.error('Invalid input:', message);
              showErrorToUser(`Invalid input: ${message}`);
              break;

            case 'NOT_FOUND':
              console.error('Resource not found');
              showErrorToUser('The requested resource was not found');
              break;

            case 'RATE_LIMITED':
              console.error('Rate limited, retrying...');
              showErrorToUser('Too many requests. Please wait a moment.');
              break;

            default:
              console.error('GraphQL error:', message);
              showErrorToUser('An error occurred. Please try again.');
          }
        });
      }

      // Network errors
      if (error.networkError) {
        console.error('Network Error:', error.networkError);
        handleNetworkError();
      }
    } else {
      // Other errors
      console.error('Unexpected error:', error);
      showErrorToUser('An unexpected error occurred');
    }

    throw error;
  }
}

function handleAuthError() {
  // Clear stored tokens
  localStorage.removeItem('swiftask_access_token');
  // Redirect to login or refresh page
  window.location.reload();
}

function handleNetworkError() {
  showErrorToUser('Network error: Please check your connection');
}

function showErrorToUser(message) {
  // Display error in UI
  console.warn('User message:', message);
}

WebSocket and subscription errors

Connection errors

const wsLink = new WebSocketLink({
  uri: 'wss://graphql.swiftask.ai/graphql',
  options: {
    reconnect: true,
    reconnectionAttempts: 5,
    connectionCallback: (error) => {
      if (error) {
        console.error('WebSocket connection error:', error);
        showConnectionError('Unable to establish real-time connection');
      } else {
        console.log('WebSocket connected');
        hideConnectionError();
      }
    },
    connectionParams: async () => {
      // Refresh token if needed
      const token = await getValidToken();
      return {
        authorization: `Bearer ${token}`,
        workspaceId: workspaceId,
      };
    },
  },
});

function showConnectionError(message) {
  const banner = document.createElement('div');
  banner.className = 'connection-error-banner';
  banner.textContent = message;
  document.body.prepend(banner);
}

function hideConnectionError() {
  const banner = document.querySelector('.connection-error-banner');
  if (banner) banner.remove();
}

Subscription error handling

const MESSAGE_STREAM = gql`
  subscription OnMessageStream($sessionId: Float!) {
    onMessageStream(sessionId: $sessionId) {
      messageChunk
      botResponseMessageId
      isStoppable
    }
  }
`;

const subscribeToStreamWithErrorHandling = (client, sessionId, onChunk, onError) => {
  let fullMessage = '';
  let retryCount = 0;

  const subscribe = () => {
    return client
      .subscribe({
        query: MESSAGE_STREAM,
        variables: { sessionId },
      })
      .subscribe({
        next: (data) => {
          fullMessage += data.onMessageStream.messageChunk;
          onChunk(fullMessage, data.onMessageStream);
          retryCount = 0; // Reset on success
        },

        error: (error) => {
          console.error('Subscription error:', error);

          // Determine error type
          if (error.message?.includes('authorization')) {
            // Auth error - re-authenticate
            onError('Authentication expired. Please refresh.');
            handleAuthError();
          } else if (error.message?.includes('network')) {
            // Network error - try to reconnect
            retryCount++;
            if (retryCount < 3) {
              console.log(`Reconnecting... (${retryCount}/3)`);
              setTimeout(() => {
                subscribe();
              }, 5000);
            } else {
              onError('Connection lost. Please refresh the page.');
            }
          } else {
            // Generic error
            onError('Connection error. Please try again.');
          }
        },

        complete: () => {
          console.log('Subscription completed');
        },
      });
  };

  return subscribe();
};

// Usage
subscribeToStreamWithErrorHandling(
  client,
  sessionId,
  (fullMessage) => {
    updateUI(fullMessage);
  },
  (errorMessage) => {
    showErrorToUser(errorMessage);
  }
);

Retry strategies

Exponential backoff

Implement automatic retries with increasing delays:

class RetryManager {
  constructor(maxRetries = 3, baseDelay = 1000) {
    this.maxRetries = maxRetries;
    this.baseDelay = baseDelay;
  }

  async retry(operation, retryCount = 0) {
    try {
      return await operation();
    } catch (error) {
      if (retryCount >= this.maxRetries) {
        throw new Error(
          `Operation failed after ${this.maxRetries} retries: ${error.message}`
        );
      }

      // Calculate delay with exponential backoff
      const delay = this.baseDelay * Math.pow(2, retryCount);
      const jitter = Math.random() * 1000; // Add randomness to prevent thundering herd

      console.log(
        `Retry ${retryCount + 1}/${this.maxRetries} after ${delay + jitter}ms`
      );

      await this.sleep(delay + jitter);
      return this.retry(operation, retryCount + 1);
    }
  }

  sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}

// Usage
const retryManager = new RetryManager(3, 1000);

const result = await retryManager.retry(async () => {
  return await sendMessage(sessionId, message);
});

Conditional retry

Only retry on specific error types:

class ConditionalRetry {
  constructor(maxRetries = 3) {
    this.maxRetries = maxRetries;
  }

  shouldRetry(error) {
    // Don't retry client errors (400-499)
    if (error.status >= 400 && error.status < 500) {
      // Except 429 (rate limit)
      if (error.status !== 429) {
        return false;
      }
    }

    // Don't retry auth errors
    if (error.message?.includes('Unauthorized') || 
        error.message?.includes('Authentication failed')) {
      return false;
    }

    // Retry on network errors
    if (error instanceof TypeError || error.networkError) {
      return true;
    }

    // Retry on server errors (500+)
    if (error.status >= 500) {
      return true;
    }

    // Retry on rate limit
    if (error.status === 429) {
      return true;
    }

    return false;
  }

  async retry(operation, maxRetries = this.maxRetries) {
    let lastError;

    for (let i = 0; i < maxRetries; i++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error;

        if (!this.shouldRetry(error)) {
          throw error;
        }

        if (i < maxRetries - 1) {
          const delay = 1000 * Math.pow(2, i);
          console.log(`Retrying in ${delay}ms...`);
          await new Promise((resolve) => setTimeout(resolve, delay));
        }
      }
    }

    throw lastError;
  }
}

// Usage
const retry = new ConditionalRetry(3);

try {
  const result = await retry.retry(() => sendMessage(sessionId, message));
} catch (error) {
  console.error('Failed after retries:', error);
}

Token refresh

Implementing automatic token refresh

class TokenManager {
  constructor(clientToken) {
    this.clientToken = clientToken;
    this.clientUuid = this.getOrCreateClientUuid();
    this.accessToken = null;
    this.tokenExpiry = null;
  }

  getOrCreateClientUuid() {
    let uuid = localStorage.getItem('swiftask_client_uuid');
    if (!uuid) {
      uuid = crypto.randomUUID();
      localStorage.setItem('swiftask_client_uuid', uuid);
    }
    return uuid;
  }

  async getValidToken() {
    // Check if token is still valid
    if (this.accessToken && this.tokenExpiry > Date.now()) {
      return this.accessToken;
    }

    // Refresh token
    return await this.refreshToken();
  }

  async refreshToken() {
    console.log('Refreshing authentication token...');

    try {
      const response = await fetch(
        `https://graphql.swiftask.ai/public/widget-bot/${this.clientToken}`,
        {
          method: 'GET',
          headers: {
            'x-client-uuid': this.clientUuid,
          },
        }
      );

      if (!response.ok) {
        throw new Error(`Token refresh failed: ${response.status}`);
      }

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

      this.accessToken = data.accessToken;
      // Assume token expires in 1 hour (adjust based on actual expiry)
      this.tokenExpiry = Date.now() + 60 * 60 * 1000;

      console.log('Token refreshed successfully');
      return this.accessToken;
    } catch (error) {
      console.error('Failed to refresh token:', error);
      throw error;
    }
  }
}

// Usage with Apollo Client
const tokenManager = new TokenManager('your_client_token');

const authLink = setContext(async (_, { headers }) => {
  try {
    const token = await tokenManager.getValidToken();
    return {
      headers: {
        ...headers,
        authorization: `Bearer ${token}`,
      },
    };
  } catch (error) {
    console.error('Failed to get valid token:', error);
    // Redirect to login or show error
    throw error;
  }
});

Complete error handler class

Production-ready error handling for all operations:

class ErrorHandler {
  constructor(options = {}) {
    this.onAuthError = options.onAuthError || (() => {});
    this.onNetworkError = options.onNetworkError || (() => {});
    this.onDisplayError = options.onDisplayError || console.error;
    this.retryManager = new RetryManager(options.maxRetries || 3);
    this.shouldRetry = options.shouldRetry || (() => true);
  }

  async handleOperation(operation, context = {}) {
    try {
      return await this.retryManager.retry(operation);
    } catch (error) {
      return this.handleError(error, context);
    }
  }

  handleError(error, context = {}) {
    console.error('Error occurred:', error, 'Context:', context);

    // Network errors
    if (error.networkError || error instanceof TypeError) {
      this.onNetworkError();
      this.onDisplayError('Network error: Please check your connection');
      return null;
    }

    // GraphQL errors
    if (error.graphQLErrors?.length > 0) {
      const gqlError = error.graphQLErrors[0];
      const code = gqlError.extensions?.code;

      if (code === 'UNAUTHENTICATED') {
        this.onAuthError();
        this.onDisplayError('Authentication failed. Please refresh the page.');
      } else if (code === 'FORBIDDEN') {
        this.onDisplayError('You do not have permission to perform this action');
      } else if (code === 'BAD_USER_INPUT') {
        this.onDisplayError(`Invalid input: ${gqlError.message}`);
      } else if (code === 'NOT_FOUND') {
        this.onDisplayError('The requested resource was not found');
      } else if (code === 'RATE_LIMITED') {
        this.onDisplayError('Too many requests. Please wait a moment.');
      } else {
        this.onDisplayError('An error occurred. Please try again.');
      }

      return null;
    }

    // Generic error
    this.onDisplayError('An unexpected error occurred');
    return null;
  }

  logError(error, context) {
    // Send to logging service (e.g., Sentry, LogRocket)
    console.error('Logging error:', {
      message: error.message,
      stack: error.stack,
      context,
      timestamp: new Date().toISOString(),
    });
  }
}

// Usage
const errorHandler = new ErrorHandler({
  onAuthError: () => {
    // Re-authenticate or redirect to login
    window.location.reload();
  },
  onNetworkError: () => {
    // Show offline indicator
    showOfflineIndicator();
  },
  onDisplayError: (message) => {
    // Show error to user
    showToast(message, 'error');
  },
  maxRetries: 3,
});

// Use in your code
const result = await errorHandler.handleOperation(
  () => sendMessage(sessionId, message),
  {
    operation: 'sendMessage',
    sessionId,
    message,
  }
);

Practical error handling patterns

Input validation

function validateMessage(message) {
  if (!message || message.trim().length === 0) {
    throw new Error('Message cannot be empty');
  }

  if (message.length > 5000) {
    throw new Error('Message is too long (max 5000 characters)');
  }

  return message.trim();
}

const handleSend = async (input) => {
  try {
    const validatedMessage = validateMessage(input);
    await sendMessage(sessionId, validatedMessage);
  } catch (error) {
    showErrorToUser(error.message);
  }
};

Session validation

async function getSessionSafely(client, sessionId) {
  try {
    const { data } = await client.query({
      query: GET_SESSION,
      variables: { sessionId },
    });

    if (!data.getOneTodoChatSession) {
      throw new Error('Session not found');
    }

    return data.getOneTodoChatSession;
  } catch (error) {
    if (error.message === 'Session not found') {
      showErrorToUser('This chat session no longer exists');
    } else {
      showErrorToUser('Failed to load session');
    }
    throw error;
  }
}

Graceful degradation

async function sendMessageWithFallback(client, sessionId, message) {
  try {
    // Try streaming first
    return await sendMessage(client, sessionId, message);
  } catch (error) {
    console.warn('Streaming failed, trying synchronous request:', error);

    try {
      // Fall back to synchronous request
      return await sendSyncMessage(client, sessionId, message);
    } catch (fallbackError) {
      console.error('Both methods failed:', fallbackError);
      throw fallbackError;
    }
  }
}

Best practices

  1. Always handle errors gracefully β€” Never let errors crash your application

  2. Provide user-friendly messages β€” Technical errors should be logged, not shown to users

  3. Implement retry logic β€” Network issues are common; retry with exponential backoff

  4. Log errors for debugging β€” Include context and stack traces for troubleshooting

  5. Monitor error rates β€” Track errors to identify systemic issues

  6. Handle token expiration β€” Implement automatic token refresh

  7. Test error scenarios β€” Simulate network failures, auth errors, etc. in development

  8. Show loading states β€” Keep users informed during retries

  9. Provide fallback options β€” Allow users to retry manually

  10. Clean up resources β€” Unsubscribe from WebSockets on errors

  11. Use error boundaries β€” In React, wrap components in error boundaries

  12. Implement circuit breakers β€” Stop retrying if errors persist

Testing error scenarios

// Simulate network error
const mockNetworkError = () => {
  throw new TypeError('Failed to fetch');
};

// Simulate GraphQL error
const mockGraphQLError = () => {
  const error = new Error('GraphQL Error');
  error.graphQLErrors = [
    {
      message: 'Unauthenticated',
      extensions: { code: 'UNAUTHENTICATED' },
    },
  ];
  throw error;
};

// Simulate rate limit
const mockRateLimitError = () => {
  const error = new Error('Rate Limited');
  error.graphQLErrors = [
    {
      message: 'Too many requests',
      extensions: { code: 'RATE_LIMITED' },
    },
  ];
  throw error;
};

// Test your error handler
test('handles network errors', async () => {
  const handler = new ErrorHandler();
  const result = await handler.handleOperation(mockNetworkError);
  expect(result).toBeNull();
});

test('handles auth errors', async () => {
  const handler = new ErrorHandler({
    onAuthError: jest.fn(),
  });
  await handler.handleOperation(mockGraphQLError);
  expect(handler.onAuthError).toHaveBeenCalled();
});

test('retries on transient errors', async () => {
  let attempts = 0;
  const operation = async () => {
    attempts++;
    if (attempts < 3) throw new Error('Temporary error');
    return 'success';
  };

  const result = await new RetryManager().retry(operation);
  expect(result).toBe('success');
  expect(attempts).toBe(3);
});

Summary

Error Type

Cause

Detection

Fix

Network

Connection issues

TypeError, networkError

Retry with backoff

Auth

Invalid/expired token

UNAUTHENTICATED code

Refresh token or re-authenticate

Input

Invalid data

BAD_USER_INPUT code

Validate input, show error

Not found

Resource missing

NOT_FOUND code

Verify IDs, show message

Rate limit

Too many requests

RATE_LIMITED code

Wait and retry

Server

Internal error

500+ status

Retry with backoff

WebSocket

Connection lost

Subscription error

Reconnect automatically