Skip to main content

Handling Results and API Errors

This project uses a structured approach to handle API responses and errors, ensuring that the frontend can gracefully manage both successful data retrieval and various failure states (such as validation errors or authentication lapses).

The API Response Structure

All requests made through the generated client return or throw objects based on two core structures: ApiResult and ApiError. These are defined in frontend/src/client/core/.

Successful Results: ApiResult

The ApiResult type represents the raw response from the server. While most service methods return the body directly, the underlying client uses this structure to track metadata about the request.

// frontend/src/client/core/ApiResult.ts
type ApiResult<TData = any> = {
readonly body: TData;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly url: string;
};

Error States: ApiError

When a request fails (e.g., a 4xx or 5xx status code), the client throws an ApiError. This class extends the standard JavaScript Error but includes critical context about the failed request, such as the response body (which often contains FastAPI validation details) and the original request options.

// frontend/src/client/core/ApiError.ts
class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: unknown;
public readonly request: ApiRequestOptions;

constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
super(message);
this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
}

Global Error Handling

The application implements a global interception strategy for authentication-related errors. In frontend/src/main.tsx, the QueryClient is configured with a QueryCache and MutationCache that monitor all background requests.

If an ApiError returns a 401 (Unauthorized) or 403 (Forbidden) status, the application automatically clears the local session and redirects the user to the login page.

// frontend/src/main.tsx
const handleApiError = (error: Error) => {
if (error instanceof ApiError && [401, 403].includes(error.status)) {
localStorage.removeItem("access_token")
window.location.href = "/login"
}
}

const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: handleApiError,
}),
mutationCache: new MutationCache({
onError: handleApiError,
}),
})

Local Error Handling and UI Feedback

For non-authentication errors (like validation failures or business logic errors), the project provides utility functions in frontend/src/utils.ts to parse the error and display it to the user via toast notifications.

Extracting Error Messages

FastAPI typically returns error details in a detail field. If the error is a validation error, detail is often an array of objects. The extractErrorMessage function handles these variations to find a human-readable string.

// frontend/src/utils.ts
function extractErrorMessage(err: ApiError): string {
if (err instanceof AxiosError) {
return err.message
}

const errDetail = (err.body as any)?.detail
if (Array.isArray(errDetail) && errDetail.length > 0) {
// Extract the first validation error message
return errDetail[0].msg
}
return errDetail || "Something went wrong."
}

Using the handleError Utility

The handleError utility is designed to be bound to a notification function (like showErrorToast). This pattern allows for concise error handling in TanStack Query mutations.

// Example usage in a component (e.g., frontend/src/components/Admin/AddUser.tsx)
const mutation = useMutation({
mutationFn: (data: UserCreate) => UsersService.createUser({ requestBody: data }),
onSuccess: () => {
showSuccessToast("User created successfully")
},
// Binds the toast function to the utility
onError: handleError.bind(showErrorToast),
})

In this pattern, handleError receives the ApiError, extracts the message using extractErrorMessage, and then calls the bound function (the toast) with that message.

Summary of Flow

  1. Request Initiation: A service method (e.g., UsersService.createUser) is called.
  2. Failure: The server returns a 400 Bad Request.
  3. Exception: The client throws an ApiError containing the server's JSON response in error.body.
  4. Global Check: handleApiError in main.tsx checks if it's a 401/403 (it isn't).
  5. Local Catch: The onError callback in the component's mutation triggers.
  6. UI Notification: handleError parses the ApiError.body.detail and displays the specific validation message to the user.