How to Handle Errors in Kaneo: Backend, Frontend and UI Best Practices

Kaneo handles errors through a layered architecture where the backend throws HTTPException status codes, the frontend parses errors into a unified ApiError type, and UI components render contextual troubleshooting guidance.

Kaneo's error handling is implemented across two monorepo packages: the Hono-based API in apps/api and the React-based web client in apps/web. The system separates backend signaling from frontend presentation, ensuring users receive actionable feedback without exposing internal implementation details.

Backend Error Handling with HTTPException

The Kaneo API uses Hono's HTTPException as the primary error signaling mechanism. Controllers throw exceptions with explicit HTTP status codes and optional messages, giving the frontend clear context for categorization.

Throwing HTTPException in Controllers

In apps/api/src/task/controllers/update-task.ts, the update task controller demonstrates multiple error conditions:

import { HTTPException } from 'hono/http-exception';
import { getTaskById } from '@/services/task';

export async function updateTask(c) {
  const { id } = c.req.valid('param');
  const data = await c.req.json();

  const task = await getTaskById(id);
  if (!task) {
    throw new HTTPException(404, { message: 'Task not found' });
  }

  if (!c.get('userId')) {
    throw new HTTPException(401, { message: 'Unauthorized' });
  }

  // …perform update…
  return c.json({ success: true });
}

Common HTTPException patterns in Kaneo include:

  • 400 Bad Request – Validation failures (e.g., apps/api/src/utils/validate-dates.ts)
  • 401 Unauthorized – Missing or expired authentication
  • 403 Forbidden – Permission violations
  • 404 Not Found – Missing resources
  • 500 Internal Server Error – Unexpected server failures

The authentication middleware in apps/api/src/utils/authenticate-api-request.ts similarly converts auth failures into HTTPException responses, ensuring consistent error signaling across all API endpoints.

Frontend Error Parsing with error-handler.ts

The web client centralizes error processing in apps/web/src/lib/error-handler.ts. This module transforms raw fetch errors into a predictable ApiError shape with five classification types.

The ApiError Type and parseApiError Function

The parseApiError() function analyzes caught errors and returns a structured object:

// Type definition inferred from implementation
type ApiErrorType = 'network' | 'cors' | 'auth' | 'server' | 'unknown';

interface ApiError {
  type: ApiErrorType;
  message: string;
  original?: Error; // Preserved for debugging
}

The parser classifies errors by examining:

  • Network errors – navigator.onLine checks, TypeError from failed connections
  • CORS errors – Specific error message patterns from fetch failures
  • Auth errors – 401/403 status codes with auth-related context
  • Server errors – 5xx status codes from the API
  • Unknown errors – Fallback for unclassified exceptions

Troubleshooting Helper Functions

The error handler exports two functions for generating user-facing guidance:

  • getCorsTroubleshootingSteps() – Returns array of CORS configuration checks
  • getNetworkTroubleshootingSteps() – Returns connectivity debugging steps

These enable UI components to display contextual help without hardcoding diagnostic content.

Integrating Error Handling in Data Fetching

Basic Fetcher with Error Handling

import { parseApiError, getCorsTroubleshootingSteps, getNetworkTroubleshootingSteps } from '@/lib/error-handler';
import { toast } from '@/lib/toast';

export async function fetchTasks(projectId: string) {
  try {
    const resp = await fetch(`${import.meta.env.VITE_API_URL}/projects/${projectId}/tasks`);
    if (!resp.ok) {
      // Wrap non-2xx responses so parseApiError can categorize them
      throw new Error(`HTTP ${resp.status}`);
    }
    return await resp.json();
  } catch (rawErr) {
    const err = parseApiError(rawErr);
    
    toast.error(err.message, {
      // Conditionally show troubleshooting steps based on error type
      details: err.type === 'cors' 
        ? getCorsTroubleshootingSteps()
        : err.type === 'network' 
          ? getNetworkTroubleshootingSteps()
          : [],
    });
    
    // Re-throw for higher-level handling if needed
    throw err;
  }
}

TanStack Query Integration

import { useQuery } from '@tanstack/react-query';
import { fetchTasks } from '@/fetchers/task';

export function useProjectTasks(projectId: string) {
  return useQuery(
    ['tasks', projectId], 
    () => fetchTasks(projectId), 
    {
      onError: (err) => {
        // err is already typed as ApiError via parseApiError
        console.error('Task fetch failed', err);
      },
      retry: false, // Let UI handle retry after showing help
    }
  );
}

The retry: false configuration prevents automatic retries that would mask intermittent errors, allowing the UI to present troubleshooting steps first.

UI Presentation and Toast Notifications

The apps/web/src/lib/toast.ts utility integrates with the error handler to display contextual messages. When ApiError.type indicates CORS or network problems, the toast renders expandable help sections using the troubleshooting arrays.

Presentation patterns include:

  • Network errors – "Unable to connect to the server. Check your internet connection."
  • CORS errors – "Configuration problem detected. [View troubleshooting steps]"
  • Auth errors – Prompt re-authentication with direct login link
  • Server errors – Generic message with error ID for support reference

Error Handling Architecture Principles

Kaneo's error handling follows four core design principles visible in the source code:

  1. Consistent error shape – All client code works with ApiError, eliminating type guards and defensive coding
  2. Backend-driven status codes – HTTP status codes from HTTPException directly map to ApiError.type classifications
  3. Granular troubleshooting – Helper functions return actionable step arrays, not just static strings
  4. Separation of concerns – Backend never leaks stack traces; frontend never inspects server internals

Summary

Frequently Asked Questions

How does Kaneo distinguish between CORS and network errors?

The parseApiError() function in apps/web/src/lib/error-handler.ts examines error message patterns and browser signals. CORS errors produce specific fetch failure messages, while network errors trigger different exception types or navigator.onLine state changes. Both map to distinct ApiError.type values with corresponding troubleshooting arrays.

What HTTP status codes should backend controllers throw for common errors?

Per Kaneo's implementation in apps/api/src/task/controllers/update-task.ts and related files: use 400 for validation failures, 401 for authentication problems, 403 for authorization violations, 404 for missing resources, and 500 for unexpected server errors. Each should include a clear message for user display.

How can I add retry logic that respects Kaneo's error classification?

Check err.type before retrying. Skip retries for auth errors (requires user action), cors errors (configuration fix needed), and often unknown errors. Consider limited retries only for network and transient server errors. The error handler preserves the original error object in err.original for advanced conditional logic.

Where is the toast UI component configured to show troubleshooting steps?

The toast utility in apps/web/src/lib/toast.ts accepts a details array option populated from getCorsTroubleshootingSteps() or getNetworkTroubleshootingSteps() based on the ApiError.type. This pattern appears in fetchers throughout apps/web/src/fetchers/ when calling toast.error().

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →