Skip to main content

Error Handling & Status Codes

Centralized, fail‑safe error handling – predictable JSON errors for every scenario.

AppError Zod Mongoose


📖 Table of Contents


Standard Error Payload

Every error response follows this exact JSON structure:

{
"success": false,
"statusCode": 401,
"message": "Invalid or expired refresh token. Please log in again.",
"data": null,
"timestamp": "2026-05-22T14:30:00.000Z",
"stack": "Error: ...at AuthService.refreshSession..." // ONLY in development
}
FieldTypeDescription
successbooleanAlways false for errors
statusCodenumberHTTP status code (400, 401, 403, etc.)
messagestringFrontend‑friendly, human‑readable error description
datanullAlways null for errors
timestampstring (ISO 8601)Server time when error occurred
stackstringDevelopment only – never shown in production

Frontend tip: Use response.data.success to determine success/failure. The message is safe to display directly in toast notifications.


HTTP Status Code Dictionary

CodeNameCommon ScenariosExample message
400Bad RequestZod validation fails, malformed JSON, invalid ObjectId"Validation Failed: email: Invalid email format"
401UnauthorizedMissing/expired/invalid access token"Invalid or expired token. Please login again."
403ForbiddenInsufficient role, email not verified, account banned"You do not have permission to perform this action"
404Not FoundRoute not found, database document missing"Product not found"
409ConflictDuplicate key (email, SKU), business rule violation"Email already registered. Please login instead."
429Too Many RequestsRate limit exceeded (see Rate Limiting)"Too many requests from this IP, please try again after 15 minutes"
500Internal Server ErrorUnhandled exception – report to backend team"Something went wrong on our side."
503Service UnavailableRedis/MongoDB unreachable"Service temporarily unavailable. Please try again later."

Validation Errors (Zod)

When a request fails Zod validation (e.g., missing field, wrong type), the API returns 400 with a comma‑separated list of failures:

{
"success": false,
"statusCode": 400,
"message": "Validation Failed: email: Invalid email format, password: Password must be at least 8 characters",
"data": null,
"timestamp": "2026-05-22T14:30:05.000Z"
}

Common validation errors per module:

ModuleTypical validation failures
AuthInvalid email, weak password, missing firstname
ProductsInvalid itemType, missing polymorphic required fields
AddressInvalid pincode (not 6 digits), missing street
OrdersNegative quantity, invalid address ID

The error message is safe to display directly. No need to parse further.


Business Logic Errors (409 Conflict)

These occur when a request violates a business rule or unique constraint.

Examples:

Scenariomessage
Duplicate email on registration"Email already registered. Please login instead."
Duplicate SKU on product creation"Product with this SKU already exists."
Order already dispatched"This order has already been dispatched. Cannot modify."
Review already submitted"You have already submitted a review for this product."

Frontend action: Show the message and guide the user (e.g., “Login instead” or “View your existing review”).


Authentication Errors (401/403)

StatusScenarioFrontend Action
401Access token missing or expiredAttempt silent refresh; if fails, redirect to login
401Refresh token blacklisted or expiredRedirect to login (session permanently dead)
403Email not verifiedShow verification prompt, resend OTP
403Account banned (isActive: false)Show support contact message
403User trying admin endpointRedirect to customer dashboard or show “Access denied”

See Authentication Guide for token refresh logic.


Rate Limiting Errors (429)

When a rate limit is exceeded, the API returns 429 with a Retry-After header (seconds to wait).

Response example:

{
"success": false,
"statusCode": 429,
"message": "Too many authentication attempts. Your IP has been temporarily blocked.",
"data": null,
"timestamp": "2026-05-22T14:30:00.000Z"
}

Headers:

Retry-After: 900
RateLimit-Limit: 10
RateLimit-Remaining: 0
RateLimit-Reset: 1748592000

Frontend action: Wait for Retry-After seconds before retrying. Do not retry immediately.
See Rate Limiting Guide for full details.


Server Errors (500/503)

StatusMeaningWhat to do
500Unhandled exception in backendReport to backend team with timestamp and request details. Retry later.
503Downstream service (Redis, MongoDB) unreachableWait and retry with backoff. If persists, inform support.

In development, the stack field is included – never expose this in production. The global error handler strips it when NODE_ENV=production.


Frontend Handling Example (Axios)

Here is a complete interceptor that handles all error types gracefully.

import axios from 'axios';

const api = axios.create({ baseURL: process.env.API_URL });

// Response interceptor
api.interceptors.response.use(
(response) => response,
async (error) => {
const { response, config } = error;
const originalRequest = config;

// 1. Handle 401 (expired access token)
if (response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const { data } = await axios.post(`${api.defaults.baseURL}/auth/refresh`);
// Store new access token (in memory)
setAccessToken(data.data.accessToken);
originalRequest.headers.Authorization = `Bearer ${data.data.accessToken}`;
return api(originalRequest);
} catch (refreshError) {
// Refresh failed – redirect to login
clearAccessToken();
window.location.href = '/login';
return Promise.reject(refreshError);
}
}

// 2. Handle 429 (rate limit) – wait and retry
if (response?.status === 429) {
const retryAfter = response.headers['retry-after'] || 15;
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
return api(originalRequest);
}

// 3. All other errors – show message to user
if (response?.data?.message) {
// Display in toast notification
showToast(response.data.message, 'error');
}

return Promise.reject(error);
}
);

export default api;

Backend Developer Guide

If you are contributing to the backend, never use res.status(400).send(...). Always throw an AppError.

Importing

import { AppError } from '@shared/utils/app-error';
import { HTTP_STATUS } from '@shared/constant/http-codes';

Usage in Services / Controllers

// Example: Checking if a user exists
const user = await User.findById(userId);
if (!user) {
throw new AppError(HTTP_STATUS.NOT_FOUND, 'User not found');
}

if (!user.isActive) {
throw new AppError(HTTP_STATUS.FORBIDDEN, 'This account has been deactivated.');
}

// Example: Business rule violation
if (cart.items.length === 0) {
throw new AppError(HTTP_STATUS.BAD_REQUEST, 'Cannot checkout with an empty cart');
}

Automatic Conversions (Global Error Handler)

The global error handler (@shared/middlewares/error.middleware.ts) automatically converts:

Raw ErrorConverted StatusMessage
Mongoose CastError (invalid ObjectId)400"Invalid ID format"
Mongoose duplicate key error (code 11000)409"Duplicate field value: {field}"
JsonWebTokenError401"Invalid token"
TokenExpiredError401"Token expired"

You do not need to catch these manually – they are handled centrally.



Predictable errors, happy frontend developers – the Reshma‑Core error handling philosophy.