# Error Response Guide

Author: Nguyen Thi Nguyet Que Created on: May 3, 2026 Updated: May 3, 2026 — Standardized error
handling documentation

## Overview

The system uses the **Chain of Responsibility** architecture to handle and return errors to the
client. All error responses go through the `response.ts` middleware to ensure consistent formatting.

---

## ✅ A. Error Response Format

### A1. Response Components

```typescript
{
  "success": false,
  "trace_id": "uuid-string",
  "timestamp": "2026-05-03T04:23:00.000Z",
  "errors": [
    {
      "field": "email",
      "code": "INVALID_EMAIL_FORMAT",
      "messages": {
        "vi": "Email không hợp lệ",
        "en": "Invalid email"
      },
      "severity": "low"
    }
  ]
}
```

### A2. Field Explanations

- **success**: Always `false` when there is an error
- **trace_id**: UUID to track the request in logs
- **timestamp**: Time when the response was created
- **errors**: Array of error objects
  - **field**: Name of the field with the error (empty for non-validation errors)
  - **code**: Error code from the error catalog
  - **messages**: Object containing multilingual messages `{ vi: string, en: string }`
  - **severity**: Error severity level (`low`, `medium`, `high`, or `critical`)

---

## 🤖 B. How to Return Errors from Controller

### B1. When to use `throw` vs `return res.error()`

- **`throw`** → In controllers with try-catch blocks. The error will be caught and passed to
  `res.error(error)`
- **`return res.error()`** → In middleware/handlers without try-catch, return the error response
  immediately

### B2. Using `throw` + `try-catch` (Standard Pattern for Controller)

```typescript
import { AuthenticationError, ValidationError, NotFoundError, Errors } from '#interfaces/error';

// In controller handler
handler: async (req: Req, res: Res) => {
	try {
		// Business logic can throw errors
		const file = await fileProvider.getById({ id: req.params.id });

		if (!file) {
			throw Errors.fileNotFound(); // → will be caught below
		}

		return sendSuccess(res, file);
	} catch (error) {
		return res.error(error); // Automatically handles and returns standard format
	}
};
```

### B3. Using `return res.error()` Directly (Pattern for Middleware/Service)

```typescript
// In middleware or handler without try-catch
handler: async (req: Req, res: Res) => {
	const isValid = await validateToken(req.headers.authorization);

	if (!isValid) {
		// Return error immediately, don't throw
		return res.error(new GenericError('UNAUTHORIZED'));
	}

	// Or use Errors factory
	return res.error(Errors.forbidden());
};
```

### B4. Automatic Validation Error Return with Zod

```typescript
import { validateZod } from '#middlewares/validators';
import { RegisterBodySchema } from '#contracts/auth/schema';

export default (_express: Application) => {
	return <Resource>{
		post: {
			middleware: [validateZod(RegisterBodySchema)] as any,
			handler: async (req: Req, res: Res) => {
				// req.body has been validated and parsed
				// If validation fails, automatically returns 422 with errors
			},
		},
	};
};
```

---

## 📋 C. Error Types and Usage

### C1. Authentication Errors

```typescript
import { AuthenticationError, Errors } from '#interfaces/error';

// Using class
throw new AuthenticationError('INVALID_CREDENTIALS');
throw new AuthenticationError('TOKEN_EXPIRED');
throw new AuthenticationError('ACCOUNT_LOCKED');

// Or using factory
throw Errors.invalidCredentials();
throw Errors.tokenExpired();
throw Errors.accountLocked();
throw Errors.sessionExpired();
```

**HTTP Status Codes**:

- `INVALID_CREDENTIALS`: 401
- `TOKEN_EXPIRED`: 401
- `ACCOUNT_LOCKED`: 403
- `ACCOUNT_INACTIVE`: 403

### C2. Authorization Errors

```typescript
import { AuthorizationError, Errors } from '#interfaces/error';

throw new AuthorizationError('FORBIDDEN');
throw new AuthorizationError('INSUFFICIENT_PERMISSIONS');

// Or factory
throw Errors.forbidden();
throw Errors.insufficientPermissions();
```

**HTTP Status Codes**:

- `FORBIDDEN`: 403
- `INSUFFICIENT_PERMISSIONS`: 403

### C3. Validation Errors

#### C3.1 Single Validation Error

```typescript
import { ValidationError, Errors } from '#interfaces/error';

// Using class with field errors
throw new ValidationError([
	{
		field: 'email',
		code: 'INVALID_EMAIL_FORMAT',
		message: { vi: 'Email không hợp lệ', en: 'Invalid email' },
		severity: 'low',
	},
]);

// Or using factory
throw Errors.invalidEmail('email');
throw Errors.invalidUuid('id');
throw Errors.passwordTooShort('password');
```

#### C3.2 Validation Error with Multiple Fields

```typescript
throw new ValidationError([
	{
		field: 'email',
		code: 'INVALID_EMAIL_FORMAT',
		message: { vi: 'Email không hợp lệ', en: 'Invalid email' },
	},
	{
		field: 'password',
		code: 'PASSWORD_TOO_SHORT',
		message: { vi: 'Mật khẩu quá ngắn', en: 'Password is too short' },
	},
]);
```

**HTTP Status Code**: 422

### 4. Not Found Errors

```typescript
import { NotFoundError, Errors } from '#interfaces/error';

// Using class with resource name
throw new NotFoundError('Người dùng');
throw new NotFoundError('File');
throw new NotFoundError('Tài nguyên');

// Or using factory
throw Errors.notFound('Resource');
throw Errors.userNotFound();
throw Errors.fileNotFound();
```

**HTTP Status Code**: 404

### C5. Conflict Errors

```typescript
import { ConflictError, Errors } from '#interfaces/error';

throw new ConflictError('USER_EXISTS');
throw new ConflictError('CONFLICT');

// Or factory
throw Errors.userExists();
throw Errors.conflict();
```

**HTTP Status Codes**:

- `USER_EXISTS`: 409
- `CONFLICT`: 409

### C6. Database Errors

```typescript
import { DatabaseError, Errors } from '#interfaces/error';

throw new DatabaseError('FOREIGN_KEY_CONSTRAINT', {
  errors: [{ field: 'userId', code: 'FOREIGN_KEY_CONSTRAINT', message: { ... } }]
});

throw new DatabaseError('UNIQUE_CONSTRAINT');

// Or factory
throw Errors.databaseError();
throw Errors.foreignKeyConstraint();
throw Errors.uniqueConstraint();
```

**HTTP Status Codes**:

- `DATABASE_ERROR`: 500
- `FOREIGN_KEY_CONSTRAINT`: 409
- `UNIQUE_CONSTRAINT`: 409

### C7. External Service Errors

```typescript
import { ExternalServiceError, Errors } from '#interfaces/error';

throw new ExternalServiceError('CIRCUIT_BREAKER_OPEN');
throw new ExternalServiceError('EXTERNAL_SERVICE_ERROR');

// Or factory
throw Errors.circuitBreakerOpen();
throw Errors.externalServiceError();
```

**HTTP Status Codes**:

- `CIRCUIT_BREAKER_OPEN`: 503
- `EXTERNAL_SERVICE_ERROR`: 502

### 8. System Errors

```typescript
import { GenericError, Errors } from '#interfaces/error';

throw new GenericError('INTERNAL_SERVER_ERROR');

// Or factory
throw Errors.internalError();
```

**HTTP Status Code**: 500

---

## 🔧 D. Validation with Zod

### D1. Schema Definition

```typescript
import { z } from 'zod';

export const RegisterBodySchema = z.object({
	email: z.string().email('Email không hợp lệ'),
	password: z.string().min(8, 'Mật khẩu phải có ít nhất 8 ký tự'),
	username: z.string().min(3).max(50),
});
```

### Using in Middleware

```typescript
import { validateZod, validateQueryZod, validateParamsZod } from '#middlewares/validators';

// Validate body
middleware: [validateZod(RegisterBodySchema)] as any;

// Validate query
middleware: [validateQueryZod(QuerySchema)] as any;

// Validate params
middleware: [validateParamsZod(ParamsSchema)] as any;
```

### D3. Zod Error Mapping

The system automatically maps Zod errors to business error codes:

| Zod Error Code           | Business Error Code    | HTTP Status |
| ------------------------ | ---------------------- | ----------- |
| `too_small` (password)   | `PASSWORD_TOO_SHORT`   | 422         |
| `too_small` (other)      | `VALUE_TOO_SHORT`      | 422         |
| `too_big`                | `VALUE_TOO_LONG`       | 422         |
| `invalid_type` (string)  | `INVALID_STRING_TYPE`  | 422         |
| `invalid_type` (number)  | `INVALID_NUMBER_TYPE`  | 422         |
| `invalid_type` (boolean) | `INVALID_BOOLEAN_TYPE` | 422         |
| `invalid_format` (email) | `INVALID_EMAIL_FORMAT` | 422         |
| `invalid_format` (uuid)  | `INVALID_UUID_FORMAT`  | 422         |
| `invalid_format` (url)   | `INVALID_URL_FORMAT`   | 422         |

---

## 🎨 E. Custom Error Handling

### E1. Custom Error with Additional Data

```typescript
throw new DatabaseError('FOREIGN_KEY_CONSTRAINT', {
	errors: [
		{
			field: 'userId',
			code: 'FOREIGN_KEY_CONSTRAINT',
			message: {
				vi: 'Người dùng được tham chiếu không tồn tại',
				en: 'Referenced user does not exist',
			},
		},
	],
	formFields: ['userId'],
	errorFields: ['userId'],
});
```

### E2. Custom Error Message

```typescript
import { NotFoundError } from '#interfaces/error';

throw new NotFoundError('Resource', {
	vi: 'Không tìm thấy tài nguyên này',
	en: 'This resource was not found',
});
```

## Return Success Response

### Using `sendSuccess()`

```typescript
import { sendSuccess } from '#utils/responseUtils';

handler: async (req: Req, res: Res) => {
	const result = await SomeService.getData();
	return sendSuccess(res, result);
};
```

### F2. Using `res.sendOk()`

```typescript
handler: async (req: Req, res: Res) => {
	const result = await SomeService.getData();
	return res.sendOk({ data: result, statusCode: 200 });
};
```

---

## 📝 G. Complete Examples

### G1. Controller with Error Handling

```typescript
import { Application } from 'express';
import { Resource } from 'express-automatic-routes';
import { Req, Res } from '#interfaces/IApi';
import { AuthService } from '#services/auth';
import { validateZod } from '#middlewares/validators';
import { LoginBodySchema } from '#contracts/auth/schema';
import { AuthenticationError, NotFoundError } from '#interfaces/error';
import { sendSuccess } from '#utils/responseUtils';

export default (_express: Application) => {
	return <Resource>{
		post: {
			middleware: [validateZod(LoginBodySchema)] as any,
			handler: async (req: Req, res: Res) => {
				try {
					const { email, password } = req.body;

					// Business logic can throw errors
					const loginResult = await AuthService.login({ email, password });

					return sendSuccess(res, loginResult);
				} catch (error) {
					// The response.ts middleware will automatically handle the error
					// and return the standard format
					return res.error(error);
				}
			},
		},
	};
};
```

### G2. Controller Throwing Error Directly (with try-catch)

```typescript
handler: async (req: Req, res: Res) => {
	try {
		const user = await UserService.findById(req.params.id);

		if (!user) {
			throw new NotFoundError('Người dùng');
		}

		if (!user.isActive) {
			throw new AuthenticationError('ACCOUNT_INACTIVE');
		}

		return sendSuccess(res, user);
	} catch (error) {
		return res.error(error);
	}
};
```

---

## 📚 H. Error Codes Reference

See the file `src/constants/errorCatalog.ts` for the complete list of error codes.

### H1. Categories

- **AUTHENTICATION**: Authentication errors (login, token, session)
- **AUTHORIZATION**: Authorization errors (permission, role)
- **VALIDATION**: Data validation errors
- **NOT_FOUND**: Resource not found errors
- **CONFLICT**: Data conflict errors
- **DATABASE**: Database errors
- **EXTERNAL**: External service errors
- **SYSTEM**: System errors

---

## 🔍 I. Logging

Errors with HTTP status >= 500 are automatically logged by the `response.ts` middleware. Controllers
do not need to call a separate logger for server errors.

```typescript
// Middleware automatically logs these errors
if (statusCode >= 500) {
	logger.logErrorAsync(`${req.method} ${req.path}`, error, { requestId: traceId });
}
```

---

## ✅ J. Best Practices

### J1. Always Use Error Classes from `#interfaces/error`

```typescript
// ✅ Good
throw new AuthenticationError('INVALID_CREDENTIALS');

// ❌ Bad - throw plain Error
throw new Error('Invalid credentials');
```

### J2. Use Factory Methods for Common Errors

```typescript
// ✅ Good
throw Errors.invalidCredentials();
throw Errors.userNotFound();

// ❌ Bad - hardcode
throw new AuthenticationError('INVALID_CREDENTIALS');
```

### J3. Throw Error from Service, Catch in Controller

```typescript
// Service
class UserService {
	async findById(id: string) {
		const user = await User.findByPk(id);
		if (!user) {
			throw new NotFoundError('Người dùng');
		}
		return user;
	}
}

// Controller
handler: async (req: Req, res: Res) => {
	try {
		const user = await UserService.findById(req.params.id);
		return sendSuccess(res, user);
	} catch (error) {
		return res.error(error);
	}
};
```

### 4. Use Zod Validation for Input

```typescript
// ✅ Good - automatic validation
middleware: [validateZod(RegisterBodySchema)] as any;

// ❌ Bad - manual validation
handler: async (req: Req, res: Res) => {
	if (!req.body.email || !isValidEmail(req.body.email)) {
		throw Errors.invalidEmail();
	}
};
```

### J5. Return Additional Data for Validation Errors

```typescript
throw new ValidationError([
	{
		field: 'email',
		code: 'INVALID_EMAIL_FORMAT',
		message: { vi: 'Email không hợp lệ', en: 'Invalid email' },
		severity: 'low',
	},
]);
```

---

## ⚠️ K. Legacy Methods (Deprecated)

The following methods are deprecated and should not be used:

```typescript
// ❌ Deprecated
res.sendError({ err: error });
res.sendErrorStatus({ status: 404, err: error });
MeUError class
```

Use instead:

```typescript
// ✅ Recommended
res.error(error);
// or
throw new GenericError('ERROR_CODE');
// or
throw Errors.someError();
```
