API Error Handling

1. Tổng quan về API Error Handling
API Error Handling là kỹ thuật xử lý lỗi trong quá trình giao tiếp giữa các thành phần của hệ thống thông qua API (Application Programming Interface). Kỹ thuật này bao gồm việc phát hiện lỗi, phân loại nguyên nhân, trả về thông tin phù hợp, ghi nhận nhật ký và giúp phía Client có thể phản ứng chính xác khi một yêu cầu không được xử lý thành công.
Trong các ứng dụng hiện đại, API thường đóng vai trò trung gian giữa Frontend và Backend. Khi người dùng đăng nhập, tìm kiếm sản phẩm, đặt hàng hoặc cập nhật thông tin cá nhân, Frontend sẽ gửi HTTP Request đến Backend. Backend tiếp nhận yêu cầu, thực hiện xử lý nghiệp vụ, truy vấn cơ sở dữ liệu hoặc gọi đến các dịch vụ bên ngoài, sau đó trả về HTTP Response.
Trong quá trình này, nhiều tình huống có thể xảy ra như dữ liệu đầu vào không hợp lệ, người dùng chưa đăng nhập, không đủ quyền truy cập, tài nguyên không tồn tại, cơ sở dữ liệu gặp sự cố hoặc dịch vụ bên ngoài không phản hồi. Nếu hệ thống không có cơ chế xử lý lỗi phù hợp, ứng dụng có thể hiển thị thông báo khó hiểu, ngừng hoạt động hoặc làm lộ thông tin kỹ thuật nhạy cảm.
API Error Handling giúp hệ thống xử lý những tình huống này một cách có tổ chức. Thay vì để lỗi phát sinh tự do và khiến ứng dụng hoạt động không ổn định, hệ thống xác định loại lỗi, chuyển đổi thành phản hồi thống nhất và cung cấp thông tin cần thiết cho Client.
Một cơ chế xử lý lỗi tốt thường hướng đến các mục tiêu sau:
- Tính ổn định: Hạn chế việc lỗi ở một chức năng làm ảnh hưởng đến toàn bộ ứng dụng.
- Tính nhất quán: Các API sử dụng cấu trúc phản hồi lỗi thống nhất.
- Khả năng bảo trì: Developer dễ dàng xác định nguyên nhân và sửa lỗi.
- Trải nghiệm người dùng: Người dùng nhận được thông báo dễ hiểu và có hướng xử lý phù hợp.
- Bảo mật: Không tiết lộ mật khẩu, token, thông tin cơ sở dữ liệu, đường dẫn nội bộ hoặc Stack Trace.
- Khả năng giám sát: Hệ thống ghi nhận lỗi để theo dõi, phân tích và phát hiện sự cố.
API Error Handling không chỉ đơn thuần là sử dụng try-catch. Đây là một phần trong kiến trúc Backend, liên quan đến HTTP Status Code, validation, exception, middleware, logging, bảo mật và cách thiết kế hợp đồng giao tiếp giữa Client với Server.
2. Vai trò của API Error Handling trong kiến trúc ứng dụng
Trong một hệ thống thông thường, Frontend chịu trách nhiệm hiển thị giao diện và tiếp nhận thao tác của người dùng, trong khi Backend xử lý nghiệp vụ và làm việc với dữ liệu. API là cầu nối giúp hai thành phần trao đổi thông tin.
Ví dụ, khi người dùng đăng nhập vào một website, Frontend gửi email và mật khẩu đến API. Backend kiểm tra dữ liệu, xác thực tài khoản và trả về kết quả. Nếu thông tin đăng nhập không chính xác, Backend cần trả về lỗi phù hợp thay vì để yêu cầu kết thúc bất thường.
Nếu không có API Error Handling, Frontend có thể chỉ nhận được một lỗi chung chung như 500 Internal Server Error, ngay cả khi nguyên nhân thực tế là người dùng nhập sai mật khẩu. Điều này khiến việc xử lý ở phía Client thiếu chính xác.
Ngược lại, khi Backend phân loại lỗi rõ ràng, Frontend có thể dựa vào mã trạng thái để đưa ra hành động phù hợp. Ví dụ:
400 Bad Request: Yêu cầu không hợp lệ.401 Unauthorized: Chưa xác thực hoặc thông tin xác thực không hợp lệ.403 Forbidden: Không có quyền thực hiện thao tác.404 Not Found: Không tìm thấy tài nguyên.409 Conflict: Yêu cầu xung đột với trạng thái hiện tại.422 Unprocessable Content: Nội dung hợp lệ về cú pháp nhưng không đáp ứng các điều kiện xử lý.429 Too Many Requests: Gửi quá nhiều yêu cầu trong một khoảng thời gian.500 Internal Server Error: Lỗi không mong muốn phía Server.503 Service Unavailable: Dịch vụ tạm thời không thể đáp ứng yêu cầu.
Việc phân loại chính xác giúp Frontend không phải suy đoán nguyên nhân dựa trên nội dung thông báo.
Đối với Backend, cơ chế này còn giúp phân biệt lỗi nghiệp vụ với lỗi hệ thống. Chẳng hạn, một sản phẩm hết hàng là tình huống nghiệp vụ có thể dự đoán trước, trong khi việc kết nối cơ sở dữ liệu thất bại là sự cố hạ tầng cần được theo dõi và xử lý riêng.
3. Quy trình hoạt động của API Error Handling
Quy trình xử lý lỗi có thể được tổ chức thành nhiều bước, từ lúc Client gửi yêu cầu đến khi Server trả về kết quả.
3.1. Tiếp nhận HTTP Request
Client gửi yêu cầu đến một Endpoint cụ thể. Request thường bao gồm HTTP Method, URL, Header, Query Parameters và Request Body.
Ví dụ:
POST /api/auth/login
Content-Type: application/json
{
"email": "[email protected]",
"password": "123456"
}
Server tiếp nhận yêu cầu và kiểm tra các điều kiện ban đầu như định dạng dữ liệu, trường bắt buộc, quyền truy cập và trạng thái hệ thống.
3.2. Kiểm tra và xác thực dữ liệu
Backend kiểm tra dữ liệu đầu vào trước khi thực hiện nghiệp vụ. Nếu email không đúng định dạng hoặc mật khẩu bị thiếu, hệ thống có thể trả về lỗi validation.
{
"success": false,
"message": "Dữ liệu đầu vào không hợp lệ",
"errors": {
"email": "Email không đúng định dạng",
"password": "Mật khẩu là trường bắt buộc"
}
}
Việc kiểm tra dữ liệu cần được thực hiện ở Backend ngay cả khi Frontend đã có validation. Client có thể bị chỉnh sửa hoặc gửi Request trực tiếp, vì vậy Backend không được mặc định rằng dữ liệu nhận được luôn hợp lệ.
3.3. Thực hiện xử lý nghiệp vụ
Sau khi vượt qua bước kiểm tra ban đầu, Backend tiến hành các thao tác như xác thực tài khoản, tính toán giá trị, kiểm tra tồn kho hoặc truy vấn dữ liệu.
Trong giai đoạn này, lỗi có thể phát sinh do điều kiện nghiệp vụ không được đáp ứng hoặc do một thành phần kỹ thuật gặp sự cố.
Ví dụ, API đặt hàng có thể phát hiện sản phẩm đã hết hàng. Đây là tình huống có thể dự đoán và cần được chuyển thành phản hồi nghiệp vụ phù hợp.
3.4. Phát hiện và phân loại lỗi
Khi một thao tác thất bại, hệ thống cần xác định nguyên nhân và phân loại lỗi. Các loại phổ biến bao gồm lỗi dữ liệu đầu vào, lỗi xác thực, lỗi phân quyền, lỗi tài nguyên, lỗi nghiệp vụ và lỗi hạ tầng.
Việc phân loại cần dựa trên bản chất của sự cố thay vì gán mọi lỗi về một mã trạng thái duy nhất.
3.5. Chuyển đổi lỗi thành HTTP Response
Backend chuyển lỗi nội bộ thành HTTP Response có Status Code và cấu trúc dữ liệu phù hợp.
Thông tin trả về Client cần đủ để ứng dụng biết yêu cầu thất bại và có thể xử lý tiếp. Tuy nhiên, các chi tiết nội bộ như câu lệnh SQL, thông tin kết nối cơ sở dữ liệu hoặc Stack Trace không nên xuất hiện trong phản hồi công khai.
3.6. Ghi nhận nhật ký và giám sát
Đối với những lỗi quan trọng, Backend ghi nhận thông tin cần thiết như thời gian, Endpoint, loại lỗi, Request ID và Stack Trace trong hệ thống logging nội bộ.
Nhờ đó, Developer có thể tìm ra nguyên nhân mà không cần yêu cầu người dùng cung cấp thông tin kỹ thuật phức tạp.
4. Phân loại lỗi trong API
Việc phân loại lỗi là nền tảng để thiết kế một hệ thống xử lý lỗi nhất quán. Mỗi nhóm lỗi cần có cách xử lý riêng nhằm tránh nhầm lẫn giữa vấn đề do người dùng và sự cố của hệ thống.
4.1. Lỗi Client
Lỗi Client thường xuất phát từ Request không hợp lệ, thiếu thông tin, sai định dạng hoặc không đáp ứng điều kiện truy cập.
Ví dụ:
- Thiếu trường email khi đăng nhập.
- Gửi ID sản phẩm không đúng định dạng.
- Truy cập tài nguyên không tồn tại.
- Gửi yêu cầu khi chưa có thông tin xác thực.
- Vượt quá giới hạn số lần gửi Request.
Những lỗi này thường sử dụng các Status Code thuộc nhóm 4xx.
Backend nên trả về thông báo có thể hành động được, chẳng hạn yêu cầu người dùng nhập lại trường còn thiếu hoặc đăng nhập trước khi tiếp tục.
4.2. Lỗi Server
Lỗi Server xảy ra khi Backend không thể hoàn thành yêu cầu do sự cố kỹ thuật hoặc tình huống không mong muốn.
Một số nguyên nhân phổ biến:
- Kết nối cơ sở dữ liệu thất bại.
- Một biến hoặc đối tượng có giá trị không như dự kiến.
- Dịch vụ bên ngoài ngừng phản hồi.
- Lỗi lập trình chưa được xử lý.
- Hết tài nguyên hoặc cấu hình hệ thống không chính xác.
Những lỗi này thường sử dụng Status Code thuộc nhóm 5xx.
Client không nên nhận được thông tin nội bộ quá chi tiết. Thay vào đó, Server có thể trả về thông báo chung và một mã tham chiếu để đội ngũ kỹ thuật tra cứu log.
4.3. Lỗi nghiệp vụ
Lỗi nghiệp vụ xuất hiện khi Request có thể hợp lệ về mặt kỹ thuật nhưng không đáp ứng quy tắc của ứng dụng.
Ví dụ:
- Tài khoản không đủ số dư để thanh toán.
- Người dùng cố gắng mua sản phẩm đã hết hàng.
- Đơn hàng đã bị hủy nhưng vẫn được yêu cầu xác nhận.
- Người dùng áp dụng mã giảm giá đã hết hạn.
Đây không nhất thiết là lỗi lập trình. Hệ thống cần xác định tình huống nào có thể dự đoán và trả về phản hồi phù hợp.
Tùy vào nghiệp vụ và hợp đồng API, lỗi có thể sử dụng 400, 403, 409 hoặc 422. Không nên sử dụng một Status Code duy nhất cho mọi trường hợp mà không xem xét ngữ nghĩa.
4.4. Lỗi phụ thuộc dịch vụ bên ngoài
Một Backend có thể sử dụng nhiều dịch vụ như cổng thanh toán, dịch vụ gửi email, hệ thống lưu trữ tệp hoặc API của bên thứ ba.
Khi dịch vụ phụ thuộc gặp sự cố, Backend cần xác định lỗi có thể thử lại hay không, thời gian chờ đã vượt giới hạn chưa và liệu yêu cầu có thể được xử lý an toàn khi gửi lại hay không.
Ví dụ, nếu cổng thanh toán tạm thời không phản hồi, hệ thống không nên mặc định rằng giao dịch chắc chắn thất bại. Giao dịch có thể đã được xử lý nhưng phản hồi bị thất lạc. Trong trường hợp này, cần có cơ chế kiểm tra trạng thái giao dịch hoặc sử dụng idempotency key để hạn chế việc thanh toán trùng lặp.
5. HTTP Status Code trong xử lý lỗi API
HTTP Status Code là mã số cho biết kết quả xử lý Request. Việc lựa chọn đúng mã trạng thái giúp Client hiểu bản chất của phản hồi và quyết định hành động tiếp theo.
| Status Code | Ý nghĩa | Trường hợp sử dụng |
|---|---|---|
200 OK |
Thành công | Request được xử lý thành công |
201 Created |
Tạo tài nguyên thành công | Tạo người dùng hoặc đơn hàng mới |
204 No Content |
Thành công, không có nội dung trả về | Xóa tài nguyên thành công |
400 Bad Request |
Request không hợp lệ | Thiếu dữ liệu hoặc sai cấu trúc |
401 Unauthorized |
Chưa xác thực hợp lệ | Token thiếu, sai hoặc hết hạn |
403 Forbidden |
Không được phép truy cập | Không đủ quyền thực hiện thao tác |
404 Not Found |
Không tìm thấy tài nguyên | ID không tồn tại |
405 Method Not Allowed |
HTTP Method không được hỗ trợ | Gọi POST vào Endpoint chỉ cho phép GET |
409 Conflict |
Xung đột trạng thái | Trùng dữ liệu hoặc trạng thái tài nguyên không phù hợp |
413 Content Too Large |
Nội dung Request quá lớn | Tải lên tệp vượt giới hạn |
415 Unsupported Media Type |
Kiểu dữ liệu không được hỗ trợ | Gửi định dạng Body không được chấp nhận |
422 Unprocessable Content |
Không xử lý được nội dung | Dữ liệu không đáp ứng điều kiện nghiệp vụ hoặc validation |
429 Too Many Requests |
Quá nhiều Request | Vượt giới hạn Rate Limit |
500 Internal Server Error |
Lỗi nội bộ Server | Ngoại lệ không mong muốn |
502 Bad Gateway |
Gateway nhận phản hồi không hợp lệ | Proxy hoặc Gateway nhận lỗi từ upstream |
503 Service Unavailable |
Dịch vụ tạm thời không khả dụng | Bảo trì hoặc quá tải |
504 Gateway Timeout |
Gateway chờ upstream quá lâu | Dịch vụ phụ thuộc phản hồi chậm |
Không nên sử dụng 200 OK cho tất cả trường hợp, kể cả khi nghiệp vụ thất bại. Nếu Server luôn trả về 200, Client phải phân tích thêm nội dung Response để biết yêu cầu thực sự thành công hay thất bại. Điều này làm giảm ý nghĩa của HTTP và gây khó khăn khi tích hợp với các công cụ giám sát.
Cũng cần phân biệt 401 và 403. 401 liên quan đến trạng thái xác thực, trong khi 403 thể hiện rằng yêu cầu không được phép thực hiện. Tuy nhiên, một số hệ thống có thể cố ý dùng phản hồi chung để tránh tiết lộ tài nguyên nhạy cảm.
6. Thiết kế cấu trúc Error Response thống nhất
Một API nên sử dụng cấu trúc phản hồi lỗi nhất quán giữa các Endpoint. Điều này giúp Frontend chỉ cần xây dựng một cơ chế xử lý chung thay vì viết logic riêng cho từng API.
Ví dụ cấu trúc lỗi:
{
"success": false,
"code": "VALIDATION_ERROR",
"message": "Dữ liệu đầu vào không hợp lệ",
"errors": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Email không đúng định dạng"
}
],
"requestId": "req_7f31a2"
}
Các trường có thể bao gồm:
success: Cho biết thao tác có thành công hay không.code: Mã lỗi ổn định để Client phân loại.message: Thông báo dễ hiểu cho người sử dụng hoặc Developer tùy theo thiết kế.errors: Danh sách lỗi chi tiết theo trường dữ liệu hoặc quy tắc nghiệp vụ.requestId: Mã định danh Request để đối chiếu với log.timestamp: Thời điểm xảy ra lỗi nếu hệ thống có nhu cầu.path: Endpoint phát sinh lỗi, khi việc cung cấp thông tin này phù hợp với chính sách bảo mật.
Không phải API nào cũng cần tất cả trường trên. Điều quan trọng là xác định một hợp đồng phản hồi rõ ràng và duy trì tính nhất quán giữa các phiên bản API.
Trong thực tế, có thể sử dụng định dạng Problem Details theo RFC 9457 để chuẩn hóa cách biểu diễn lỗi HTTP. Định dạng này sử dụng các trường như type, title, status, detail và instance, đồng thời cho phép mở rộng thêm những trường phù hợp với ứng dụng.
Ví dụ:
{
"type": "https://api.example.com/problems/validation-error",
"title": "Validation Error",
"status": 422,
"detail": "Một hoặc nhiều trường dữ liệu không hợp lệ",
"instance": "/api/users",
"errors": {
"email": [
"Email không đúng định dạng"
]
}
}
Khi thiết kế Error Response, cần tránh đưa các thông tin như mật khẩu, token, chuỗi kết nối, dữ liệu cá nhân không cần thiết hoặc Stack Trace vào phản hồi công khai.
7. Xử lý lỗi bằng try-catch
try-catch là cơ chế phổ biến trong nhiều ngôn ngữ lập trình, giúp chương trình bắt và xử lý ngoại lệ phát sinh trong quá trình thực thi.
Khối try chứa đoạn mã có khả năng phát sinh lỗi. Khối catch tiếp nhận ngoại lệ và thực hiện hành động xử lý tương ứng. Một số ngôn ngữ còn hỗ trợ finally để thực hiện thao tác dọn dẹp bất kể có xảy ra lỗi hay không.
Ví dụ JavaScript:
async function getUser(userId) {
try {
const user = await userRepository.findById(userId);
if (!user) {
const error = new Error("Không tìm thấy người dùng");
error.statusCode = 404;
throw error;
}
return user;
} catch (error) {
console.error("Lỗi khi lấy thông tin người dùng:", error);
throw error;
}
}
Trong ví dụ này, nếu người dùng không tồn tại, chương trình chủ động tạo ngoại lệ với mã trạng thái 404. Nếu Repository phát sinh lỗi, ngoại lệ cũng được chuyển tiếp đến tầng xử lý bên ngoài.
Tuy nhiên, try-catch không nên được sử dụng một cách máy móc cho mọi đoạn mã. Nếu bắt lỗi rồi chỉ in ra Console mà không trả kết quả phù hợp hoặc không chuyển tiếp lỗi, hệ thống có thể che giấu nguyên nhân thật sự.
Ví dụ không tốt:
async function createUser(data) {
try {
return await userRepository.create(data);
} catch (error) {
console.log(error);
return null;
}
}
Đoạn mã trên biến mọi lỗi thành null, khiến tầng gọi hàm không phân biệt được giữa trường hợp không có dữ liệu và trường hợp thao tác tạo người dùng thất bại.
Cách tiếp cận tốt hơn là để ngoại lệ được chuyển đến một tầng xử lý tập trung hoặc chuyển thành loại lỗi nghiệp vụ đã được định nghĩa rõ ràng.
8. Global Error Handler và Middleware
Global Error Handler là cơ chế xử lý lỗi tập trung ở cấp ứng dụng. Thay vì mỗi Endpoint tự định dạng Error Response, các lỗi được chuyển đến một thành phần chung để phân loại, ghi log và trả phản hồi.
Middleware là một thành phần trung gian trong chuỗi xử lý Request. Middleware có thể kiểm tra xác thực, ghi log, giới hạn tốc độ, kiểm tra dữ liệu hoặc xử lý lỗi tùy theo vị trí và thiết kế của nó.
Trong Express.js, Error Middleware thường nhận bốn tham số: err, req, res và next.
Ví dụ:
function errorHandler(err, req, res, next) {
const statusCode = err.statusCode || 500;
if (statusCode >= 500) {
console.error({
message: err.message,
stack: err.stack,
path: req.originalUrl
});
}
res.status(statusCode).json({
success: false,
code: err.code || "INTERNAL_SERVER_ERROR",
message:
statusCode >= 500
? "Đã xảy ra lỗi hệ thống"
: err.message,
requestId: req.requestId
});
}
module.exports = errorHandler;
Có thể đăng ký Middleware sau các Route:
const express = require("express");
const app = express();
app.use(express.json());
// Khai báo các Route tại đây.
// Đặt Error Middleware sau các Route.
app.use(errorHandler);
Nếu Route sử dụng Promise hoặc async/await, cần bảo đảm lỗi bất đồng bộ được chuyển đến Error Middleware. Express 5 tự động chuyển tiếp lỗi từ Promise bị reject trong Route Handler; với Express 4, thông thường cần next(error) hoặc một wrapper phù hợp.
Trong môi trường thực tế, cần bổ sung việc kiểm tra headersSent, xử lý các loại lỗi đã biết, ghi log có cấu trúc và tránh trả thông báo nội bộ ra Client.
Ưu điểm lớn của Global Error Handler là giảm sự trùng lặp, tạo quy chuẩn thống nhất và giúp việc bảo trì trở nên dễ dàng hơn. Khi cần thay đổi cấu trúc Error Response, Developer có thể cập nhật tại một nơi thay vì chỉnh sửa hàng chục Endpoint.
9. Xây dựng Custom Error Class
Custom Error Class là cách tạo các loại ngoại lệ riêng để mô tả chính xác tình huống xảy ra trong ứng dụng. Thay vì sử dụng Error chung cho mọi trường hợp, Backend có thể định nghĩa những loại lỗi như ValidationError, NotFoundError, UnauthorizedError hoặc ConflictError.
Ví dụ:
class AppError extends Error {
constructor(message, statusCode = 500, code = "INTERNAL_ERROR") {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.code = code;
this.isOperational = true;
Error.captureStackTrace?.(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(message = "Không tìm thấy tài nguyên") {
super(message, 404, "NOT_FOUND");
}
}
class ValidationError extends AppError {
constructor(message = "Dữ liệu không hợp lệ") {
super(message, 422, "VALIDATION_ERROR");
}
}
module.exports = {
AppError,
NotFoundError,
ValidationError
};
Khi sử dụng:
const { NotFoundError } = require("./errors");
async function getProduct(productId) {
const product = await productRepository.findById(productId);
if (!product) {
throw new NotFoundError("Không tìm thấy sản phẩm");
}
return product;
}
Cách tiếp cận này làm cho mã nguồn dễ đọc hơn. Khi nhìn thấy NotFoundError, Developer có thể nhận biết ngay đây là lỗi tài nguyên không tồn tại thay vì phải kiểm tra một chuỗi thông báo tùy ý.
Tuy nhiên, Custom Error Class cần được thiết kế hợp lý. Không nên tạo quá nhiều lớp lỗi nhỏ không có ý nghĩa riêng biệt. Một hệ thống vừa đủ thường có lớp lỗi cơ sở và các loại lỗi quan trọng gắn với quy tắc nghiệp vụ.
Ngoài ra, không nên để mọi lỗi kế thừa AppError mặc nhiên trở thành lỗi có thể công khai. Dữ liệu phản hồi phải được quyết định theo loại lỗi và chính sách bảo mật.
10. Validation Error và kiểm tra dữ liệu đầu vào
Validation là quá trình xác minh dữ liệu có đáp ứng các quy tắc mà hệ thống yêu cầu hay không. Đây là lớp bảo vệ quan trọng trước khi Backend thực hiện nghiệp vụ.
Các quy tắc validation thường bao gồm:
- Trường bắt buộc không được để trống.
- Email phải đúng định dạng.
- Mật khẩu đáp ứng yêu cầu về độ dài và độ phức tạp.
- Giá sản phẩm phải nằm trong giới hạn cho phép.
- Số lượng đặt hàng phải là số nguyên dương.
- ID phải đúng kiểu dữ liệu.
- Giá trị Enum phải thuộc danh sách cho phép.
- Kích thước và định dạng tệp tải lên phải được giới hạn.
Ví dụ sử dụng Zod trong Node.js:
const { z } = require("zod");
const createUserSchema = z.object({
name: z.string().trim().min(2).max(100),
email: z.string().trim().email(),
age: z.number().int().min(18).max(100)
});
function validateCreateUser(data) {
const result = createUserSchema.safeParse(data);
if (!result.success) {
return {
success: false,
errors: result.error.issues.map((issue) => ({
field: issue.path.join("."),
message: issue.message
}))
};
}
return {
success: true,
data: result.data
};
}
Phương thức safeParse() trả về kết quả thành công hoặc thất bại mà không cần sử dụng ngoại lệ cho mỗi lần validation không đạt yêu cầu.
Khi validation thất bại, Backend có thể trả về danh sách lỗi theo từng trường để Frontend hiển thị ngay bên dưới ô nhập liệu tương ứng.
Cần phân biệt validation với kiểm tra nghiệp vụ. Ví dụ, quantity phải là số nguyên dương là quy tắc validation; sản phẩm còn đủ tồn kho hay không là điều kiện nghiệp vụ. Hai bước này liên quan với nhau nhưng không thay thế cho nhau.
Validation cũng không thể thay thế các biện pháp bảo mật như phân quyền, truy vấn tham số hóa hoặc kiểm tra quyền sở hữu tài nguyên.
11. Authentication Error và Authorization Error
Authentication là quá trình xác minh danh tính người dùng. Authorization là quá trình xác định người dùng đã xác thực có quyền thực hiện một hành động cụ thể hay không.
Hai khái niệm này thường xuất hiện trong các API yêu cầu đăng nhập.
Ví dụ, khi người dùng gửi Request nhưng không có token hợp lệ, Backend có thể trả về 401 Unauthorized. Khi người dùng đã đăng nhập nhưng không có quyền xóa một sản phẩm quản trị, Backend có thể trả về 403 Forbidden.
Ví dụ phản hồi:
{
"success": false,
"code": "UNAUTHORIZED",
"message": "Vui lòng đăng nhập để tiếp tục"
}
Đối với lỗi phân quyền:
{
"success": false,
"code": "FORBIDDEN",
"message": "Bạn không có quyền thực hiện thao tác này"
}
Trong hệ thống sử dụng JWT, Backend cần kiểm tra chữ ký, thời hạn và các điều kiện xác thực của token. Không nên chỉ giải mã token rồi mặc định rằng dữ liệu bên trong là đáng tin cậy.
Bên cạnh đó, hệ thống phải kiểm tra quyền trên từng tài nguyên cần bảo vệ. Một người dùng đã đăng nhập không đồng nghĩa với việc họ có quyền truy cập thông tin của mọi tài khoản khác.
Đối với các tài nguyên nhạy cảm, đôi khi hệ thống lựa chọn trả về 404 thay vì 403 để hạn chế tiết lộ sự tồn tại của tài nguyên. Đây là quyết định thiết kế cần nhất quán và phù hợp với mô hình bảo mật.
12. Database Error Handling
Database Error Handling là quá trình xử lý các lỗi phát sinh khi Backend giao tiếp với cơ sở dữ liệu.
Các sự cố thường gặp bao gồm:
- Kết nối cơ sở dữ liệu bị gián đoạn.
- Truy vấn vượt thời gian chờ.
- Vi phạm ràng buộc
UNIQUE. - Vi phạm khóa ngoại.
- Dữ liệu không đúng kiểu.
- Deadlock hoặc tranh chấp tài nguyên.
- Giao dịch không thể hoàn tất.
Không phải mọi lỗi cơ sở dữ liệu đều nên trả về 500. Chẳng hạn, nếu email đã tồn tại và hệ thống có quy tắc ngăn tạo tài khoản trùng lặp, Backend có thể chuyển lỗi ràng buộc thành phản hồi 409 Conflict.
Ví dụ xử lý lỗi ở tầng nghiệp vụ:
async function registerUser(data) {
try {
return await userRepository.create(data);
} catch (error) {
if (error.code === "23505") {
throw new AppError(
"Email đã được sử dụng",
409,
"EMAIL_ALREADY_EXISTS"
);
}
throw error;
}
}
Trong ví dụ trên, mã 23505 thường được PostgreSQL sử dụng cho lỗi vi phạm ràng buộc duy nhất. Khi áp dụng, cần xác minh tên ràng buộc bị vi phạm để phân biệt email trùng với các trường dữ liệu khác.
Lỗi không được nhận diện cần được chuyển tiếp đến cơ chế xử lý chung, thay vì cố gắng chuyển mọi ngoại lệ thành lỗi nghiệp vụ.
Đối với các thao tác gồm nhiều bước, Transaction giúp bảo đảm tính toàn vẹn dữ liệu. Ví dụ, khi tạo đơn hàng, hệ thống có thể cần ghi thông tin đơn hàng, chi tiết sản phẩm và cập nhật tồn kho. Nếu một bước quan trọng thất bại, Transaction có thể được Rollback để tránh dữ liệu ở trạng thái không nhất quán.
Tuy nhiên, Transaction của cơ sở dữ liệu không tự động hoàn tác được những tác động đã xảy ra bên ngoài, chẳng hạn một khoản thanh toán đã được cổng thanh toán xử lý. Các quy trình phân tán cần cơ chế bù trừ, đối soát hoặc xử lý trạng thái riêng.
13. Async Error Handling trong Node.js
Node.js thường xử lý các thao tác I/O theo mô hình bất đồng bộ. Những thao tác như truy vấn cơ sở dữ liệu, đọc tệp và gọi HTTP API có thể trả về Promise.
Vì vậy, xử lý lỗi bất đồng bộ là một phần quan trọng của API Error Handling.
Ví dụ:
async function getProducts(req, res, next) {
try {
const products = await productService.getAll();
return res.status(200).json({
success: true,
data: products
});
} catch (error) {
return next(error);
}
}
Nếu productService.getAll() trả về Promise bị reject, khối catch tiếp nhận lỗi và chuyển nó đến Error Middleware.
Một lỗi phổ biến là quên await hoặc không xử lý Promise bị reject. Khi đó, lỗi có thể không được chuyển đến cơ chế xử lý mà Developer mong đợi.
Ví dụ:
async function createOrder(req, res, next) {
try {
orderService.create(req.body);
return res.status(201).json({
success: true,
message: "Đã tiếp nhận yêu cầu"
});
} catch (error) {
return next(error);
}
}
Đoạn mã trên có vấn đề vì không await Promise từ orderService.create(). Server có thể trả về thành công trước khi thao tác tạo đơn hàng hoàn tất, và lỗi bất đồng bộ có thể không được khối catch tiếp nhận.
Cách viết phù hợp hơn:
async function createOrder(req, res, next) {
try {
const order = await orderService.create(req.body);
return res.status(201).json({
success: true,
data: order
});
} catch (error) {
return next(error);
}
}
Trong những hệ thống phức tạp, có thể sử dụng wrapper để giảm lặp lại try-catch ở các Route. Tuy nhiên, cần hiểu rõ hành vi của phiên bản Framework đang sử dụng và bảo đảm mọi lỗi đều được chuyển tiếp an toàn.
14. Error Handling trong Frontend
API Error Handling không chỉ là trách nhiệm của Backend. Frontend cũng cần xử lý các tình huống API thất bại để giao diện không bị treo hoặc hiển thị dữ liệu sai.
Các tình huống phổ biến gồm:
- Request đang tải dữ liệu.
- Request thành công nhưng danh sách rỗng.
- Request thất bại do dữ liệu không hợp lệ.
- Token hết hạn.
- Không có kết nối mạng.
- Server phản hồi quá chậm.
- Người dùng nhấn nút nhiều lần trong khi Request chưa hoàn tất.
Frontend nên phân biệt trạng thái loading, success, empty và error. Với mỗi trạng thái, giao diện cần có cách hiển thị phù hợp.
Ví dụ với Fetch API:
async function fetchProducts() {
try {
const response = await fetch("/api/products");
const data = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(
data?.message || `HTTP Error: ${response.status}`
);
}
return data;
} catch (error) {
console.error("Không thể tải sản phẩm:", error);
throw error;
}
}
Fetch API không tự động ném ngoại lệ chỉ vì Server trả về 400 hoặc 500. Vì vậy, cần kiểm tra response.ok hoặc response.status để nhận diện HTTP Error.
Đoạn mã trên xử lý cả trường hợp Response Body không phải JSON. Điều này hữu ích khi Server trả về nội dung rỗng hoặc một trang lỗi không đúng định dạng mong đợi.
Ở tầng giao diện, có thể hiển thị thông báo:
try {
const products = await fetchProducts();
renderProducts(products);
} catch (error) {
showErrorMessage(
error.message || "Không thể tải danh sách sản phẩm"
);
}
Trong ứng dụng thực tế, không nên hiển thị nguyên văn mọi thông báo kỹ thuật cho người dùng. Frontend có thể dựa trên mã lỗi ổn định để hiển thị nội dung phù hợp, đồng thời cung cấp nút thử lại khi tình huống cho phép.
15. Centralized Error Handling ở Frontend
Khi ứng dụng có nhiều API, việc xử lý lỗi ở từng Component sẽ làm tăng sự trùng lặp. Một giải pháp thường được sử dụng là xây dựng HTTP Client dùng chung.
HTTP Client có thể chịu trách nhiệm:
- Thiết lập Base URL.
- Thêm Header mặc định.
- Đính kèm token khi cần.
- Chuẩn hóa Response.
- Phân loại HTTP Error.
- Xử lý lỗi mạng.
- Áp dụng Timeout.
- Quản lý việc thử lại Request.
Ví dụ sử dụng Axios:
import axios from "axios";
const api = axios.create({
baseURL: "/api",
timeout: 10000,
headers: {
"Content-Type": "application/json"
}
});
api.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response) {
const status = error.response.status;
const data = error.response.data;
return Promise.reject({
type: "HTTP_ERROR",
status,
code: data?.code || "REQUEST_FAILED",
message: data?.message || "Yêu cầu thất bại"
});
}
if (error.request) {
return Promise.reject({
type: "NETWORK_ERROR",
message: "Không thể kết nối đến máy chủ"
});
}
return Promise.reject({
type: "CLIENT_ERROR",
message: "Không thể tạo yêu cầu"
});
}
);
export default api;
Interceptor giúp tập trung logic xử lý Response tại một vị trí. Các Component có thể nhận một cấu trúc lỗi thống nhất mà không cần kiểm tra nhiều trường hợp lặp lại.
Tuy nhiên, không nên tự động chuyển hướng đăng nhập cho mọi lỗi 401 mà không có cơ chế phối hợp. Nếu nhiều Request cùng hết hạn token, ứng dụng có thể tạo nhiều lần chuyển hướng hoặc nhiều lần làm mới token cùng lúc.
Đối với hệ thống sử dụng Refresh Token, cần thiết kế luồng làm mới token rõ ràng, tránh vòng lặp vô hạn và xử lý trường hợp Refresh Token cũng hết hạn.
16. Retry Mechanism và Timeout
Retry Mechanism là cơ chế gửi lại Request khi xảy ra lỗi được xác định là có thể khắc phục bằng cách thử lại. Timeout là giới hạn thời gian chờ để một thao tác hoàn tất.
Hai cơ chế này thường được sử dụng khi Backend giao tiếp với dịch vụ bên ngoài hoặc khi ứng dụng hoạt động trong môi trường mạng không ổn định.
Ví dụ, một API lấy tỷ giá có thể tạm thời không phản hồi do lỗi mạng. Nếu thao tác chỉ đọc dữ liệu và lỗi thuộc loại có thể thử lại, hệ thống có thể gửi lại Request sau một khoảng thời gian.
Một chiến lược phổ biến là Exponential Backoff, trong đó thời gian chờ tăng dần qua từng lần thử lại. Có thể kết hợp thêm Jitter để tránh nhiều Client cùng gửi lại Request vào một thời điểm.
Ví dụ minh họa:
async function retryRequest(request, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await request();
} catch (error) {
const retryable =
error.code === "ECONNRESET" ||
error.code === "ETIMEDOUT";
if (!retryable || attempt === maxRetries) {
throw error;
}
const delay = 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
Đoạn mã trên chỉ là ví dụ cơ bản. Hệ thống thực tế cần xác định rõ loại lỗi có thể thử lại, tổng thời gian cho phép và cách xử lý các phản hồi như 429 hoặc 503. Khi có Header Retry-After, cần cân nhắc tuân thủ giá trị do Server cung cấp.
Đặc biệt, không nên tự động thử lại mọi Request ghi dữ liệu. Nếu Request tạo đơn hàng hoặc thanh toán đã được xử lý nhưng phản hồi bị mất, gửi lại có thể tạo ra giao dịch trùng lặp.
Đối với thao tác không idempotent, có thể sử dụng Idempotency Key để nhận diện những yêu cầu thuộc cùng một thao tác logic. Server cần lưu và kiểm tra khóa theo một chính sách nhất quán trước khi thực hiện lại nghiệp vụ.
17. Logging và Monitoring
Logging là quá trình ghi lại các sự kiện xảy ra trong ứng dụng. Monitoring là quá trình theo dõi tình trạng hoạt động của hệ thống thông qua các chỉ số, log, cảnh báo và dữ liệu quan sát khác.
Trong API Error Handling, Logging và Monitoring giúp Developer phát hiện sự cố, xác định nguyên nhân và đánh giá ảnh hưởng đến người dùng.
Một bản ghi lỗi hữu ích có thể bao gồm:
{
"level": "error",
"timestamp": "2026-10-15T10:30:00.000Z",
"requestId": "req_7f31a2",
"method": "POST",
"path": "/api/orders",
"statusCode": 500,
"errorCode": "DATABASE_ERROR",
"message": "Không thể hoàn tất thao tác tạo đơn hàng"
}
Các thông tin này giúp Developer tìm kiếm sự cố theo Request ID, Endpoint, mã lỗi hoặc thời gian phát sinh.
Một số công cụ thường được sử dụng gồm:
- Winston: Thư viện logging cho Node.js.
- Pino: Logging có cấu trúc với hiệu năng tốt.
- Sentry: Theo dõi lỗi ứng dụng và thu thập thông tin hỗ trợ gỡ lỗi.
- Prometheus: Thu thập và truy vấn các chỉ số giám sát.
- Grafana: Trực quan hóa dữ liệu giám sát và xây dựng Dashboard.
Đây là các công cụ có vai trò khác nhau và có thể kết hợp với nhau tùy theo kiến trúc.
Logging cần tuân thủ chính sách bảo mật. Không nên ghi trực tiếp mật khẩu, Access Token, Refresh Token, dữ liệu thẻ thanh toán hoặc các thông tin cá nhân nhạy cảm. Những dữ liệu cần thiết cho việc điều tra phải được giới hạn quyền truy cập và thời gian lưu trữ.
Đối với lỗi hệ thống, Stack Trace có thể được lưu trong log nội bộ. Tuy nhiên, việc gửi Stack Trace về Client trong môi trường Production có thể làm lộ thông tin về mã nguồn và kiến trúc hệ thống.
18. Bảo mật trong API Error Handling
Xử lý lỗi có ảnh hưởng trực tiếp đến bảo mật vì phản hồi lỗi có thể tiết lộ nhiều thông tin về hệ thống.
Ví dụ, một thông báo như sau không phù hợp khi trả về Client:
{
"success": false,
"message": "Database connection failed at 10.0.0.5",
"stack": "Error at /app/src/services/userService.js:42"
}
Thông tin trên có thể tiết lộ địa chỉ nội bộ và cấu trúc mã nguồn. Trong môi trường Production, nên trả về nội dung chung hơn:
{
"success": false,
"code": "INTERNAL_SERVER_ERROR",
"message": "Đã xảy ra lỗi hệ thống",
"requestId": "req_7f31a2"
}
Một số nguyên tắc quan trọng:
Không để lộ dữ liệu nhạy cảm. Thông tin chẩn đoán chi tiết chỉ nên xuất hiện trong hệ thống log có kiểm soát.
Không tin tưởng dữ liệu từ Client. Mọi dữ liệu đầu vào cần được kiểm tra, chuẩn hóa và xác thực ở Backend.
Giới hạn số lượng Request. Rate Limiting giúp hạn chế lạm dụng API và giảm nguy cơ quá tải.
Kiểm tra quyền truy cập. Mỗi thao tác trên tài nguyên cần được kiểm tra quyền phù hợp, không chỉ dựa vào việc người dùng đã đăng nhập.
Chống Injection. Sử dụng truy vấn tham số hóa hoặc ORM đúng cách, tránh ghép chuỗi dữ liệu người dùng trực tiếp vào câu lệnh SQL.
Không tiết lộ sự tồn tại của tài nguyên nhạy cảm khi không cần thiết. Với một số Endpoint, cần cân nhắc cách trả về 403 hoặc 404 để tránh cung cấp thông tin cho đối tượng không có quyền.
Bảo vệ hệ thống logging. Log phải được giới hạn quyền truy cập, tránh ghi dữ liệu bí mật và được kiểm soát để không bị lợi dụng.
Phân biệt môi trường Development và Production. Trong môi trường phát triển, thông tin chi tiết giúp gỡ lỗi nhanh. Trong Production, phản hồi cần được giới hạn theo chính sách bảo mật.
19. API Error Handling trong kiến trúc phân tầng
Trong kiến trúc Backend, việc tổ chức xử lý lỗi nên phù hợp với trách nhiệm của từng tầng. Một hệ thống có thể sử dụng các tầng Controller, Service, Repository và Database.
Controller tiếp nhận Request, kiểm tra dữ liệu ở mức giao tiếp HTTP, gọi Service và trả Response. Controller không nên chứa quá nhiều quy tắc nghiệp vụ.
Service thực hiện logic nghiệp vụ, kiểm tra điều kiện và quyết định khi nào cần phát sinh lỗi nghiệp vụ.
Repository chịu trách nhiệm truy cập dữ liệu. Tầng này có thể phát hiện lỗi truy vấn hoặc lỗi cơ sở dữ liệu và chuyển đổi chúng khi có lý do rõ ràng.
Global Error Handler tiếp nhận lỗi đã được chuyển lên, ghi log và chuyển lỗi thành HTTP Response.
Ví dụ luồng xử lý:
Client
|
v
Controller
|
v
Service
|
v
Repository
|
v
Database
Khi có lỗi:
Repository -> Service -> Controller/Error Middleware
|
v
HTTP Error Response
|
v
Client
Trong mô hình này, tầng Service có thể phát sinh ProductNotFoundError nếu sản phẩm không tồn tại. Global Error Handler ánh xạ lỗi đó thành 404 Not Found.
Ngược lại, nếu Database bị mất kết nối, lỗi kỹ thuật cần được ghi nhận trong log và chuyển thành phản hồi phù hợp, thường là 500 hoặc 503 tùy theo bản chất sự cố và thiết kế hệ thống.
Cách tổ chức này giúp phân tách trách nhiệm và tránh để Controller phải xử lý trực tiếp mọi loại lỗi của cơ sở dữ liệu.
20. API Error Handling trong REST API
REST API thường sử dụng HTTP Method và Status Code để thể hiện mục đích của Request cũng như kết quả xử lý.
Các phương thức như GET, POST, PUT, PATCH và DELETE có ngữ nghĩa khác nhau. Việc xử lý lỗi nên nhất quán với ngữ nghĩa đó.
Ví dụ:
GET /api/products/100: Trả về404nếu sản phẩm không tồn tại.POST /api/products: Trả về400hoặc422nếu dữ liệu không đáp ứng quy tắc.POST /api/orders: Trả về lỗi nghiệp vụ phù hợp nếu sản phẩm hết hàng.PATCH /api/users/10: Trả về403nếu người dùng không có quyền cập nhật tài khoản.DELETE /api/products/100: Trả về404nếu tài nguyên không tồn tại hoặc không thể truy cập theo chính sách của hệ thống.
Một API tốt cần có tài liệu mô tả các mã lỗi có thể xảy ra ở từng Endpoint. Ví dụ, API đăng nhập có thể trả về 400 khi thiếu dữ liệu, 401 khi thông tin xác thực không hợp lệ và 429 khi vượt giới hạn yêu cầu.
Tài liệu rõ ràng giúp Frontend, Mobile App và các hệ thống tích hợp hiểu chính xác hành vi của API. Khi hợp đồng phản hồi thay đổi, cần cân nhắc khả năng tương thích ngược để tránh làm hỏng những Client đang sử dụng phiên bản cũ.
21. Kiểm thử API Error Handling
Testing là bước xác minh hệ thống phản ứng chính xác khi xảy ra lỗi. Một API không chỉ cần hoạt động đúng trong điều kiện bình thường mà còn phải xử lý tốt những tình huống bất thường.
Các loại kiểm thử có thể áp dụng:
Unit Test: Kiểm tra từng hàm hoặc Service riêng biệt, chẳng hạn Service phải phát sinh NotFoundError khi không tìm thấy sản phẩm.
Integration Test: Kiểm tra sự phối hợp giữa Controller, Service, Database và Error Middleware.
API Test: Gửi HTTP Request thực tế và xác minh Status Code, Response Body cùng các Header liên quan.
Security Test: Kiểm tra xem phản hồi lỗi có làm lộ thông tin nhạy cảm hoặc cho phép vượt qua quyền truy cập hay không.
Load Test: Đánh giá cách hệ thống phản ứng khi có lượng Request lớn hoặc dịch vụ phụ thuộc trở nên chậm.
Ví dụ kiểm thử với Jest và Supertest:
const request = require("supertest");
const app = require("../app");
describe("GET /api/products/:id", () => {
test("trả về 404 khi sản phẩm không tồn tại", async () => {
const response = await request(app)
.get("/api/products/999999");
expect(response.status).toBe(404);
expect(response.body.success).toBe(false);
expect(response.body.code).toBe("NOT_FOUND");
});
});
Bài kiểm thử trên xác minh cả mã trạng thái HTTP và cấu trúc phản hồi. Trong dự án thực tế, cần thiết lập dữ liệu kiểm thử hoặc Mock Repository phù hợp để bảo đảm kết quả ổn định.
Ngoài các trường hợp lỗi thông thường, nên kiểm thử những tình huống như Request Body sai định dạng, cơ sở dữ liệu không khả dụng, Timeout, token hết hạn và Response từ dịch vụ bên ngoài không đúng cấu trúc.
Mục tiêu của kiểm thử không chỉ là xác nhận hệ thống phát hiện lỗi, mà còn bảo đảm lỗi được chuyển đến đúng tầng, trả về đúng mã trạng thái và không làm lộ dữ liệu ngoài ý muốn.
22. Những sai lầm phổ biến khi xử lý lỗi API
22.1. Trả về cùng một Status Code cho mọi lỗi
Sử dụng 500 cho tất cả tình huống làm mất khả năng phân biệt giữa lỗi người dùng, lỗi nghiệp vụ và lỗi kỹ thuật.
Cần xác định nguyên nhân và lựa chọn Status Code phù hợp. Lỗi validation không nên bị nhầm thành sự cố hệ thống.
22.2. Bắt lỗi nhưng không xử lý
Một số đoạn mã chỉ ghi lỗi ra Console rồi tiếp tục thực hiện các bước sau. Điều này có thể khiến hệ thống trả về thành công dù thao tác thực tế đã thất bại.
Sau khi bắt lỗi, chương trình phải xử lý hợp lý, chuyển tiếp lỗi hoặc trả về kết quả được xác định rõ ràng.
22.3. Trả về toàn bộ đối tượng Error
Đối tượng Error có thể chứa Stack Trace và các thông tin nội bộ. Việc đưa toàn bộ đối tượng vào JSON Response không phù hợp với môi trường Production.
Nên chuyển đổi ngoại lệ thành một cấu trúc phản hồi được kiểm soát.
22.4. Dùng thông báo lỗi làm mã định danh
Nếu Frontend phải so sánh chuỗi như "Email đã tồn tại" để xác định loại lỗi, việc thay đổi ngôn ngữ hoặc nội dung thông báo có thể làm hỏng logic.
Nên sử dụng mã ổn định như EMAIL_ALREADY_EXISTS, còn message được dùng để hiển thị nội dung phù hợp.
22.5. Tự động Retry mọi Request
Việc gửi lại các thao tác tạo đơn hàng, chuyển tiền hoặc thanh toán có thể dẫn đến thực hiện nghiệp vụ nhiều lần.
Cần xác định Request có an toàn khi gửi lại hay không, đồng thời áp dụng Idempotency khi cần thiết.
22.6. Không có Logging và Request ID
Khi người dùng báo lỗi nhưng hệ thống không lưu thông tin liên quan, Developer khó tìm ra nguyên nhân.
Request ID giúp kết nối thông tin giữa Client, Backend và các dịch vụ phụ thuộc trong quá trình điều tra.
22.7. Xử lý lỗi không nhất quán giữa các Endpoint
Nếu mỗi API có một cấu trúc lỗi khác nhau, Frontend phải viết nhiều nhánh xử lý và dễ phát sinh lỗi.
Cần thống nhất cấu trúc Error Response, quy tắc Status Code và mã lỗi trong toàn bộ hệ thống.
22.8. Nhầm lẫn giữa lỗi dự kiến và lỗi bất ngờ
Lỗi dự kiến như dữ liệu không hợp lệ hoặc tài nguyên không tồn tại thường có thể được xử lý trực tiếp. Lỗi bất ngờ như lỗi lập trình hoặc cơ sở dữ liệu mất kết nối cần được ghi nhận và chuyển đến cơ chế xử lý chung.
Phân biệt hai nhóm giúp hệ thống không ghi log quá nhiều cho các tình huống nghiệp vụ thông thường và không bỏ sót những sự cố quan trọng.
23. Quy trình xây dựng API Error Handling trong dự án thực tế
Khi triển khai API Error Handling cho một dự án mới, có thể thực hiện theo quy trình sau.
Bước 1: Xác định các nhóm lỗi. Liệt kê lỗi validation, authentication, authorization, nghiệp vụ, cơ sở dữ liệu, dịch vụ bên ngoài và lỗi hệ thống.
Bước 2: Xây dựng quy ước Status Code. Xác định cách sử dụng các mã HTTP cho từng nhóm lỗi và áp dụng thống nhất cho các Endpoint.
Bước 3: Thiết kế Error Response. Xây dựng cấu trúc phản hồi chung gồm mã lỗi, thông báo, thông tin lỗi theo trường và Request ID khi cần thiết.
Bước 4: Tạo Custom Error Class. Định nghĩa các lỗi quan trọng để Service có thể biểu diễn chính xác những tình huống nghiệp vụ.
Bước 5: Xây dựng Global Error Handler. Tập trung việc ghi log, ánh xạ lỗi và tạo HTTP Response.
Bước 6: Triển khai Validation. Kiểm tra dữ liệu đầu vào trước khi xử lý nghiệp vụ, đồng thời bảo đảm Backend không tin tưởng dữ liệu từ Client.
Bước 7: Xử lý lỗi bất đồng bộ. Bảo đảm mọi Promise bị reject và ngoại lệ trong các thao tác bất đồng bộ được chuyển đến đúng nơi.
Bước 8: Tích hợp Logging và Monitoring. Theo dõi lỗi, xây dựng chỉ số và thiết lập cảnh báo cho các sự cố quan trọng.
Bước 9: Viết kiểm thử. Kiểm tra Status Code, Response Body, quyền truy cập, lỗi cơ sở dữ liệu và những trường hợp bất thường.
Bước 10: Hoàn thiện tài liệu API. Mô tả các loại lỗi có thể xảy ra, cấu trúc phản hồi và cách Client nên xử lý.
Thực hiện đầy đủ các bước này giúp API có quy tắc xử lý lỗi rõ ràng ngay từ đầu, giảm việc sửa đổi lặp lại khi hệ thống mở rộng và hỗ trợ nhiều Client khác nhau.
24. Ví dụ tổng hợp API Error Handling với Express.js
Ví dụ dưới đây kết hợp Custom Error Class, Controller, Route và Global Error Middleware để minh họa cách tổ chức xử lý lỗi trong một API.
const express = require("express");
const app = express();
app.use(express.json());
class AppError extends Error {
constructor(message, statusCode, code) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.isOperational = true;
}
}
const products = [
{ id: 1, name: "Keyboard", price: 500000 },
{ id: 2, name: "Mouse", price: 250000 }
];
app.get("/api/products/:id", (req, res, next) => {
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
return next(
new AppError(
"ID sản phẩm không hợp lệ",
400,
"INVALID_PRODUCT_ID"
)
);
}
const product = products.find((item) => item.id === id);
if (!product) {
return next(
new AppError(
"Không tìm thấy sản phẩm",
404,
"PRODUCT_NOT_FOUND"
)
);
}
return res.status(200).json({
success: true,
data: product
});
});
app.use((err, req, res, next) => {
if (res.headersSent) {
return next(err);
}
const statusCode = err.statusCode || 500;
if (statusCode >= 500) {
console.error({
message: err.message,
stack: err.stack,
path: req.originalUrl
});
}
return res.status(statusCode).json({
success: false,
code: err.code || "INTERNAL_SERVER_ERROR",
message:
statusCode >= 500
? "Đã xảy ra lỗi hệ thống"
: err.message
});
});
app.listen(3000, () => {
console.log("Server running at http://localhost:3000");
});
Khi gọi GET /api/products/1, API trả về thông tin sản phẩm với 200 OK.
Khi gọi GET /api/products/abc, API trả về 400 Bad Request với mã lỗi INVALID_PRODUCT_ID.
Khi gọi GET /api/products/999, API trả về 404 Not Found với mã lỗi PRODUCT_NOT_FOUND.
Nếu một lỗi kỹ thuật không được dự kiến phát sinh, Error Middleware sẽ sử dụng phản hồi 500 Internal Server Error và ghi nhận thông tin chẩn đoán ở phía Server.
Đây là ví dụ minh họa cơ chế xử lý lỗi cơ bản. Khi triển khai thực tế, cần bổ sung Request ID, validation đầy đủ, logging có cấu trúc, bảo vệ dữ liệu đầu vào và xử lý những lỗi phát sinh từ cơ sở dữ liệu hoặc dịch vụ bên ngoài.
25. Tổng kết
API Error Handling là thành phần quan trọng trong quá trình phát triển Backend và thiết kế API. Một hệ thống xử lý lỗi tốt không chỉ phát hiện sự cố mà còn phải phân loại nguyên nhân, lựa chọn HTTP Status Code chính xác, trả về cấu trúc phản hồi nhất quán và ghi nhận thông tin cần thiết để hỗ trợ bảo trì.
Các kiến thức cốt lõi bao gồm HTTP Status Code, Error Response, try-catch, Custom Error Class, Global Error Handler, Middleware, Validation, Authentication, Authorization, Database Error Handling, Async Error Handling, Retry, Timeout, Logging, Monitoring và Testing.
Trong dự án thực tế, xử lý lỗi cần được thiết kế xuyên suốt từ tầng giao tiếp HTTP đến tầng nghiệp vụ và cơ sở dữ liệu. Frontend cũng cần phối hợp để hiển thị thông báo phù hợp, quản lý trạng thái lỗi và cho phép người dùng thử lại khi an toàn.
Điểm quan trọng nhất là không xem lỗi như một tình huống ngoại lệ hiếm gặp. Lỗi là một phần bình thường của hệ thống phần mềm. Khi được thiết kế có quy tắc, API Error Handling giúp ứng dụng ổn định hơn, dễ mở rộng, an toàn hơn và thuận tiện hơn trong quá trình phát triển lâu dài.








Khoá học lập trình game con rắn cho trẻ em




