Overview

At Formbricks, we follow consistent error handling patterns to ensure reliability, debuggability, and maintainability across our codebase. This document outlines our standard approaches to error handling.

Core Principles

  1. Type Safety: Use typed errors and results
  2. Meaningful Messages: Provide clear, actionable error messages
  3. Proper Propagation: Handle or propagate errors appropriately
  4. Logging: Ensure errors are properly logged for debugging
  5. Recovery: Implement graceful fallbacks where possible

Standard Error Types

We maintain a set of standardized error types for different scenarios:

export interface ApiErrorResponse {
code:
| "not_found"
| "gone"
| "bad_request"
| "internal_server_error"
| "unauthorized"
| "method_not_allowed"
| "not_authenticated"
| "forbidden"
| "network_error";
message: string;
status: number;
url: URL;
details?: Record<string, string | string[] | number | number[] | boolean | boolean[]>;
responseMessage?: string;
}

Error Handling Patterns

API Error Handling

For API endpoints:

export const GET = async (request: Request) => {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const data = await fetchData(authentication.environmentId!);
return responses.successResponse(data);
} catch (error) {
if (error instanceof DatabaseError) {
return responses.badRequestResponse(error.message);
}
throw error;
}
};

Client-Side Error Handling

For client-side operations:

const handleOperation = async () => {
const result = await performAction();
if (!result.ok) {
logger.error(`Operation failed: ${result.error.message}`);
toast.error("Operation failed. Please try again.");
return;
}
// Process successful result
processResult(result.data);
};

Best Practices

  1. Never Swallow Errors

    • Always handle or propagate errors
    • Log errors appropriately for debugging
    • Use error boundaries in React components
  2. Type Safety

    • Use typed error responses
    • Leverage TypeScript for compile-time error checking
    • Define clear error interfaces
  3. Error Messages

    • Include relevant context in error messages
    • Make messages actionable for developers
    • Use consistent error formatting
  4. Error Recovery

    • Implement fallback behaviors where appropriate
    • Gracefully degrade functionality when possible
    • Provide user feedback for recoverable errors
  5. Documentation

    • Document expected errors in function JSDoc
    • Include error handling in code examples
    • Keep error handling documentation up to date

Testing Error Scenarios

Always include error case testing:

describe("fetchEnvironmentState()", () => {
test("returns err(...) on network error", async () => {
const mockNetworkError = {
code: "network_error",
message: "Timeout",
responseMessage: "Network fail",
};
const result = await fetchEnvironmentState();
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.code).toBe(mockNetworkError.code);
expect(result.error.message).toBe(mockNetworkError.message);
}
});
});

These standards ensure consistent, reliable error handling across the Formbricks codebase while maintaining good developer experience and system reliability.