Usage
Error Handling
Result-based error handling with normalized error messages.
chowbea-axios uses Result-based error handling—API calls return { data, error } instead of throwing exceptions.
The Result Type
Every API call returns a Result<T>:
type Result<T> =
| { data: T; error: null } // Success
| { data: null; error: ApiError }; // FailureThis makes error handling explicit and type-safe.
Basic Error Handling
import { api } from "./api/api.client";
const { data, error } = await api.get("/users/{id}", { id: "123" });
if (error) {
// error is ApiError, data is null
console.error("Request failed:", error.message);
return;
}
// TypeScript knows data is not null here
console.log("User:", data.name);ApiError Structure
The ApiError type contains:
interface ApiError {
/** Human-readable error message */
message: string;
/** Error code (NETWORK_ERROR, NOT_FOUND, etc.) */
code: string;
/** HTTP status code (null for network errors) */
status: number | null;
/** Request context for debugging */
request: RequestContext;
/** Original error response body */
details?: unknown;
}
interface RequestContext {
method: string; // "GET", "POST", etc.
url: string; // "/users/123"
baseURL?: string; // "https://api.example.com"
params?: unknown; // Query parameters
data?: unknown; // Request body (sensitive fields redacted)
}Error Codes
| Code | HTTP Status | Description |
|---|---|---|
NETWORK_ERROR | null | Network failure, no response |
TIMEOUT | null | Request timed out |
BAD_REQUEST | 400 | Invalid request |
UNAUTHORIZED | 401 | Authentication required |
FORBIDDEN | 403 | Permission denied |
NOT_FOUND | 404 | Resource not found |
CONFLICT | 409 | Resource conflict |
VALIDATION_ERROR | 422 | Validation failed |
RATE_LIMITED | 429 | Too many requests |
SERVER_ERROR | 5xx | Server error |
REQUEST_ERROR | other | Other client errors |
UNKNOWN_ERROR | - | Unexpected error |
Handling Specific Errors
const { data, error } = await api.get("/users/{id}", { id: "123" });
if (error) {
switch (error.code) {
case "NOT_FOUND":
console.log("User not found");
return null;
case "UNAUTHORIZED":
// Redirect to login
window.location.href = "/login";
return;
case "NETWORK_ERROR":
console.log("Network issue, retrying...");
// Implement retry logic
break;
default:
console.error("Unexpected error:", error.message);
throw new Error(error.message);
}
}Type Guards
Use the provided type guards for cleaner code:
import { api, isSuccess, isError } from "./api/api.client";
const result = await api.get("/users/{id}", { id: "123" });
if (isSuccess(result)) {
// result.data is typed, result.error is null
console.log(result.data.name);
}
if (isError(result)) {
// result.error is ApiError, result.data is null
console.error(result.error.message);
}Error Normalization
The client automatically extracts error messages from various API response formats:
// These all produce the same error.message:
// Format 1: { message: "User not found" }
// Format 2: { error: "User not found" }
// Format 3: { error: { message: "User not found" } }
// Format 4: { errors: ["User not found"] }
// Format 5: { errors: { email: ["Invalid email"] } }
// Format 6: { detail: "User not found" } // FastAPI
// Format 7: { title: "User not found" } // ASP.NET Problem DetailsThis normalization means you don't need to handle different backend error formats.
Accessing Raw Response
The original response data is available in error.details:
if (error) {
console.log("Normalized message:", error.message);
console.log("Raw response:", error.details);
// For validation errors, details might contain field errors
if (error.code === "VALIDATION_ERROR" && error.details) {
const details = error.details as { errors: Record<string, string[]> };
for (const [field, messages] of Object.entries(details.errors)) {
console.log(`${field}: ${messages.join(", ")}`);
}
}
}Request Context for Debugging
Every error includes context about the failed request:
if (error) {
console.log("Failed request:");
console.log(" Method:", error.request.method);
console.log(" URL:", error.request.url);
console.log(" Params:", error.request.params);
// Note: Sensitive fields in request body are redacted
console.log(" Body:", error.request.data);
}Creating Custom Errors
You can create ApiError objects manually if needed:
import { createApiError } from "./api/api.client";
try {
// Some operation that might throw
} catch (err) {
const apiError = createApiError(err);
// apiError is now a normalized ApiError
}Wrapper for Throwing Behavior
If you prefer try/catch, create a wrapper:
import { api, type ApiError } from "./api/api.client";
class ApiException extends Error {
constructor(public apiError: ApiError) {
super(apiError.message);
this.name = "ApiException";
}
}
async function fetchOrThrow<T>(
promise: Promise<{ data: T | null; error: ApiError | null }>
): Promise<T> {
const { data, error } = await promise;
if (error) throw new ApiException(error);
return data;
}
// Usage
try {
const user = await fetchOrThrow(api.get("/users/{id}", { id: "123" }));
console.log(user.name);
} catch (err) {
if (err instanceof ApiException) {
console.error(err.apiError.code);
}
}React Error Boundary Integration
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<ApiError | null>(null);
useEffect(() => {
async function loadUser() {
const { data, error } = await api.get("/users/{id}", { id: userId });
if (error) {
setError(error);
return;
}
setUser(data);
}
loadUser();
}, [userId]);
if (error) {
return <ErrorDisplay error={error} />;
}
if (!user) {
return <Loading />;
}
return <div>{user.name}</div>;
}
function ErrorDisplay({ error }: { error: ApiError }) {
if (error.code === "NOT_FOUND") {
return <div>User not found</div>;
}
if (error.code === "UNAUTHORIZED") {
return <div>Please log in to view this page</div>;
}
return <div>Error: {error.message}</div>;
}