This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.

Xử lý lỗi (error handling) là một trong những thách thức lớn nhất khi phát triển phần mềm đáng tin cậy. C++ cấp 1990 chỉ có cơ chế trả về mã lỗi (error codes), nhưng điều này dẫn đến code rườm rà, khó bảo trì, và dễ gây rò rỉ tài nguyên (resource leak). C++ hiện đại giới thiệu hệ thống Exception Handling kết hợp với RAII (Resource Acquisition Is Initialization) để đảm bảo an toàn tài nguyên ngay cả khi lỗi xảy ra. Bài học này sẽ mổ xẻ sâu các mức độ an toàn ngoại lệ, cơ chế stack unwinding, và các best practices để viết mã an toàn.

1. Tại Sao Cần Exception? — Hạn Chế Của Mã Lỗi Truyền Thống

Trước khi Exception xuất hiện, cách duy nhất xử lý lỗi là trả về một mã lỗi (error code) hoặc giá trị đặc biệt từ hàm:

error_code_approach.cpp
// Cách cũ: Trả về mã lỗi
bool readFile(const std::string& filename, std::vector<char>& buffer) {
    std::ifstream file(filename);
    if (!file.is_open()) {
        return false; // Mã lỗi: 0 = thất bại
    }
    if (!file.read(buffer.data(), buffer.size())) {
        return false; // Một mã lỗi cho hai sự kiện khác nhau!
    }
    return true; // 1 = thành công
}

// Lời gọi hàm: phải kiểm tra return value
std::vector<char> data(1024);
if (!readFile("config.txt", data)) {
    // Nó thất bại vì lý do gì? Không biết!
    // File không tồn tại? Không đủ quyền? Lỗi đọc?
    std::cerr << "Lỗi chung chung nào đó!" << std::endl;
}

Những vấn đề chính của cách tiếp cận error codes:

  • Mất thông tin: Mã lỗi (thường là 0/1 hoặc số nhỏ) không thể mô tả chi tiết nguyên nhân sai sót.
  • Code xử lý rườm rà: Cứ mỗi lời gọi hàm đều phải kiểm tra return value, làm cho code chính bị che khuất.
  • Rò rỉ tài nguyên: Dễ quên giải phóng tài nguyên (memory, file handles, database connections) khi lỗi xảy ra giữa chừng.
  • Không thể phân biệt giữa lỗi thực sự và kết quả hợp lệ: Nếu hàm trả về -1, nó có thể là lỗi hoặc là giá trị dữ liệu hợp lệ.

Exception là cơ chế tách riêng xử lý lỗi ra khỏi control flow bình thường, cho phép lập trình viên mô tả chi tiết lý do sai sót và tự động giải phóng tài nguyên thông qua RAII.

2. Try-Catch-Throw: Cơ Sở Của Exception Handling

Exception handling trong C++ có ba từ khóa chính: throw (ném ngoại lệ), try (khối bảo vệ), và catch (bắt ngoại lệ).

exception_basics.cpp
#include <iostream>
#include <stdexcept>
#include <fstream>

class FileNotFoundError : public std::exception {};

std::string readFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file.is_open()) {
        // Ném exception kèm thông tin chi tiết
        throw std::ios_base::failure(
            "Cannot open file: " + filename
        );
    }
    std::string content, line;
    while (std::getline(file, line)) {
        content += line + "\n";
    }
    return content;
}

int main() {
    try {
        // Khối mã có thể ném exception
        std::string data = readFile("missing.txt");
        std::cout << "File content:\n" << data << std::endl;
    }
    catch (const std::ios_base::failure& ex) {
        // Bắt exception cụ thể (specific type)
        std::cerr << "File I/O Error: " << ex.what() << std::endl;
    }
    catch (const std::exception& ex) {
        // Bắt lớp cơ sở (base class) - catch-all cho std exceptions
        std::cerr << "Standard Exception: " << ex.what() << std::endl;
    }
    catch (...) {
        // Catch-all cho bất kỳ ngoại lệ nào (không an toàn)
        std::cerr << "Unknown exception!" << std::endl;
    }
    return 0;
}

Quy tắc Catch: Specific First, General Last

Compiler sẽ kiểm tra các catch blocks từ trên xuống dưới và chọn block đầu tiên có kiểu match. Vì vậy, luôn để các exception types cụ thể ở trên, các generic/base classes ở dưới.

exception_catch_order.cpp
// ĐÚNG: Specific first, then generic
try {
    // some code
}
catch (const std::invalid_argument& ex) {  // Most specific
    std::cerr << "Invalid argument: " << ex.what() << std::endl;
}
catch (const std::logic_error& ex) {      // More generic
    std::cerr << "Logic error: " << ex.what() << std::endl;
}
catch (const std::exception& ex) {       // Even more generic
    std::cerr << "Standard exception: " << ex.what() << std::endl;
}
catch (...) {                               // Catch-all (last resort)
    std::cerr << "Unknown exception!" << std::endl;
}

// SAI: Generic first sẽ "ăn" cả specific exceptions!
try {
    // some code
}
catch (const std::exception& ex) {  // Cái này sẽ catch mọi std exceptions!
    // ...
}
catch (const std::invalid_argument& ex) {  // Không bao giờ chạy đến đây
    // ...
}

3. Exception Hierarchies — Thiết Kế Hệ Thống Exception Tốt

Ngôn ngữ C++ cung cấp một hệ thống exception chuẩn dựa trên lớp std::exception và các lớp con của nó. Hiểu rõ cấu trúc này rất quan trọng để thiết kế exception handling linh hoạt.

std::exception
  ├── std::logic_error          (Lỗi logic - bug trong chương trình)
  │    ├── std::invalid_argument (Đối số không hợp lệ)
  │    ├── std::out_of_range     (Chỉ số vượt phạm vi)
  │    ├── std::length_error     (Độ dài quá lớn)
  │    └── std::domain_error     (Giá trị ngoài domain toán học)
  │
  └── std::runtime_error        (Lỗi thời gian chạy - không lỗi logic)
       ├── std::range_error      (Lỗi khoảng toán học)
       ├── std::overflow_error   (Tràn số)
       └── std::underflow_error  (Dưới số)
              

Quy tắc chọn lựa:

  • std::logic_error: Khi lỗi là do bug trong code logic (VD: hàm được gọi với đối số không hợp lệ).
  • std::runtime_error: Khi lỗi là do các điều kiện thời gian chạy mà chương trình không thể kiểm soát (VD: file không tồn tại, network down).

Tạo Custom Exception Classes

Đối với các lỗi cụ thể của ứng dụng, bạn nên tạo các lớp exception tùy chỉnh được thừa kế từ std::exception hoặc các lớp con của nó:

custom_exceptions.cpp
#include <iostream>
#include <stdexcept>
#include <string>

// Custom exception cho JSON parsing errors
class JSONParseError : public std::runtime_error {
private:
    int lineNumber;
    std::string expectedToken;

public:
    JSONParseError(const std::string& message, int line,
                   const std::string& expected)
        : std::runtime_error(message),
          lineNumber(line),
          expectedToken(expected) {}

    int getLineNumber() const { return lineNumber; }
    const std::string& getExpected() const { return expectedToken; }
};

class DatabaseError : public std::runtime_error {
private:
    int errorCode;

public:
    DatabaseError(const std::string& msg, int code)
        : std::runtime_error(msg), errorCode(code) {}

    int getErrorCode() const { return errorCode; }
};

// Sử dụng custom exceptions
void parseJSON(const std::string& json) {
    if (json.empty()) {
        throw JSONParseError(
            "Empty JSON string",
            1,
            "Opening brace '{'"
        );
    }
    if (json[0] != '{') {
        throw JSONParseError(
            "Invalid JSON: expected object",
            1,
            "{");
    }
}

int main() {
    try {
        parseJSON("");
    }
    catch (const JSONParseError& ex) {
        std::cerr << "JSON Error at line " << ex.getLineNumber()
                  << ": " << ex.what() << std::endl;
        std::cerr << "Expected: " << ex.getExpected() << std::endl;
    }
    catch (const std::exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
    }
    return 0;
}

4. Exception Safety Guarantees — Mức Độ An Toàn Khi Ngoại Lệ Xảy Ra

Khi thiết kế các hàm C++, việc xác định Exception Safety Guarantee (mức độ an toàn ngoại lệ) là rất quan trọng. Có bốn mức độ an toàn được định nghĩa rõ:

Mức Độ Định Nghĩa Ví Dụ
No-Throw Không bao giờ ném exception, luôn thành công Destructor, swap()
Strong Commit-or-Rollback: nếu lỗi xảy ra, state trở về như cũ Database transaction
Basic Bất biến được duy trì nhưng state có thể thay đổi Vector::push_back nếu allocation thất bại
Weak Không đảm bảo gì cả - có thể xảy ra undefined behavior Raw pointers, malloc/free

No-Throw Guarantee (Không Bao Giờ Ném)

Hàm này đảm bảo luôn thành công, không bao giờ ném exception. Destructor phải được đặc biệt khai báo là noexcept (implicit):

no_throw_guarantee.cpp
class NoThrowExample {
private:
    int value = 0;

public:
    // Destructor: implicitly noexcept, không bao giờ ném
    ~NoThrowExample() {
        // Chỉ thực hiện các tác vụ an toàn (ghi log, reset)
    }

    // Ghi rõ noexcept - hàm này không ném exception
    void reset() noexcept {
        value = 0;
    }

    // swap() thường được implement với no-throw guarantee
    void swap(NoThrowExample& other) noexcept {
        std::swap(this->value, other.value);
    }

    int getValue() const noexcept {
        return value;
    }
};

Strong Exception Safety (Commit-or-Rollback)

Nếu hàm thất bại do exception, state của object trở về hoàn toàn như cũ (All-or-Nothing):

strong_exception_safety.cpp
#include <iostream>
#include <vector>

class StrongSafetyExample {
private:
    std::vector<int> data;
    std::string metadata;

public:
    // Phương pháp: Copy-and-swap
    // 1. Tạo bản sao
    // 2. Thay đổi bản sao
    // 3. Nếu thành công, swap với original
    // 4. Nếu thất bại, original không bị ảnh hưởng

    void updateData(const std::vector<int>& newData) {
        // Tạo bản sao
        std::vector<int> temp = data;

        // Thay đổi bản sao (có thể ném exception)
        if (newData.size() > 1000) {
            throw std::invalid_argument(
                "Data size exceeds maximum"
            );
        }
        temp = newData;
        metadata = "Updated at " + std::to_string(time(nullptr));

        // Nếu đến đây mà không ném exception, thì swap
        // Swap là no-throw operation
        swap(temp);  // temp được hủy bỏ bình thường
    }

private:
    void swap(std::vector<int>& other) noexcept {
        data.swap(other);
    }
};

Basic Exception Safety (Bất Biến Được Duy Trì)

Mọi invariant của object được duy trì, nhưng state có thể đã thay đổi một phần:

basic_exception_safety.cpp
class BasicSafetyExample {
private:
    std::vector<int> data;
    size_t count = 0;

public:
    // push_back() chỉ đảm bảo basic safety
    // Nếu allocation thất bại, object vẫn trong trạng thái hợp lệ
    // (không hỏng bất biến), nhưng dữ liệu mới không được thêm vào

    void push_back(int value) {
        // Nếu memory allocation thất bại ở đây,
        // vector vẫn hợp lệ, nhưng value không được thêm
        try {
            data.push_back(value);
            count++;
        }
        catch (const std::bad_alloc&) {
            // Object vẫn ở trạng thái hợp lệ (Basic Safety)
            // nhưng count không tăng
            std::cerr << "Memory allocation failed" << std::endl;
            // Rethrow hoặc xử lý
            throw;
        }
    }

    size_t getCount() const noexcept {
        return count;
    }
};

5. RAII & Exception Safety — Tối Ưu Hóa Giải Phóng Tài Nguyên

RAII (Resource Acquisition Is Initialization) là mẫu thiết kế C++ mạnh mẽ nhất để đảm bảo tài nguyên luôn được giải phóng đúng cách, ngay cả khi exception xảy ra. Ý tưởng chính: tài nguyên được cấp phát trong constructor và được giải phóng trong destructor.

Khi exception xảy ra, C++ runtime thực hiện stack unwinding: nó lần lượt gọi destructor của tất cả các objects trên stack theo thứ tự ngược lại (LIFO - Last In First Out).

[STACK UNWINDING DIAGRAM]

Function call stack:
┌─────────────────────┐
│ main()              │ ← Execution stops here
│ ├─ obj1 (Resource)  │
│ ├─ obj2 (Resource)  │
│ └─ obj3 (Resource)  │ ← EXCEPTION THROWN!
└─────────────────────┘

Exception caught:
~obj3() called  ← Resource freed
~obj2() called  ← Resource freed
~obj1() called  ← Resource freed
(catch block executes safely)
              

Ví Dụ: Database Transaction Với RAII

Một ứng dụng thực tế của RAII: database transaction rollback khi lỗi xảy ra:

database_raii.cpp
#include <iostream>
#include <stdexcept>

class DatabaseConnection {
private:
    bool isConnected = false;
    bool inTransaction = false;

public:
    void connect() {
        std::cout << "[DB] Connected" << std::endl;
        isConnected = true;
    }

    void disconnect() {
        if (isConnected) {
            std::cout << "[DB] Disconnected" << std::endl;
            isConnected = false;
        }
    }

    void beginTransaction() {
        if (!isConnected) throw std::runtime_error("Not connected");
        std::cout << "[DB] BEGIN TRANSACTION" << std::endl;
        inTransaction = true;
    }

    void commit() {
        if (!inTransaction) throw std::runtime_error("No active transaction");
        std::cout << "[DB] COMMIT" << std::endl;
        inTransaction = false;
    }

    void rollback() {
        if (!inTransaction) throw std::runtime_error("No active transaction");
        std::cout << "[DB] ROLLBACK" << std::endl;
        inTransaction = false;
    }
};

// RAII wrapper cho transaction
class Transaction {
private:
    DatabaseConnection& db;
    bool committed = false;

public:
    Transaction(DatabaseConnection& connection) : db(connection) {
        db.beginTransaction();
    }

    ~Transaction() {
        // Destructor: rollback nếu chưa commit
        if (!committed) {
            std::cout << "[RAII] Destructor: rolling back uncommitted transaction" << std::endl;
            db.rollback();
        }
    }

    void commit() {
        db.commit();
        committed = true;
    }
};

// Scope guard cho connection
class ConnectionGuard {
private:
    DatabaseConnection& db;

public:
    ConnectionGuard(DatabaseConnection& connection) : db(connection) {
        db.connect();
    }

    ~ConnectionGuard() {
        db.disconnect();
    }
};

int main() {
    DatabaseConnection db;

    try {
        ConnectionGuard connGuard(db);  // Tự động connect/disconnect

        {
            Transaction txn(db);  // BEGIN TRANSACTION

            // Thực hiện các truy vấn
            std::cout << "[App] Updating user data..." << std::endl;

            // Giả sử xảy ra lỗi
            throw std::runtime_error("Database constraint violation!");

            // Không bao giờ đến đây
            txn.commit();  // Nếu đến đây, transaction được commit
        }  // RAII: ~Transaction() gọi rollback() tự động!
    }
    catch (const std::exception& ex) {
        std::cerr << "[Error] " << ex.what() << std::endl;
    }
    // RAII: ~ConnectionGuard() gọi disconnect() tự động!

    std::cout << "[App] Program ended safely" << std::endl;
    return 0;
}

/* OUTPUT:
[DB] Connected
[DB] BEGIN TRANSACTION
[App] Updating user data...
[RAII] Destructor: rolling back uncommitted transaction
[DB] ROLLBACK
[Error] Database constraint violation!
[DB] Disconnected
[App] Program ended safely
*/

6. Exception Handling Patterns & Alternatives

Exception không phải là cách duy nhất xử lý lỗi. Dưới đây là các mẫu khác nhau và khi nên sử dụng chúng:

Exceptions vs std::optional (C++17)

std::optional<T> được dùng khi một giá trị có thể có hoặc không có, không phải do lỗi nghiêm trọng:

optional_vs_exceptions.cpp
#include <optional>
#include <string>

// Sử dụng exceptions: Lỗi nghiêm trọng
double divideWithException(double a, double b) {
    if (b == 0.0) {
        throw std::invalid_argument("Division by zero");
    }
    return a / b;
}

// Sử dụng optional: Giá trị có thể có hoặc không
std::optional<double> divideWithOptional(double a, double b) {
    if (b == 0.0) {
        return std::nullopt;  // Không có kết quả
    }
    return a / b;  // Có kết quả
}

// Sử dụng optional cho tìm kiếm
std::optional<std::string> findUserEmail(int userId) {
    // Giả sử tìm kiếm user trong database
    if (userId < 0) {
        return std::nullopt;  // User không tồn tại
    }
    return "[email protected]";
}

int main() {
    // Cách sử dụng optional
    if (auto result = divideWithOptional(10.0, 2.0)) {
        std::cout << "Result: " << result.value() << std::endl;
    } else {
        std::cout << "Division by zero" << std::endl;
    }

    // Cách sử dụng exception
    try {
        double result = divideWithException(10.0, 2.0);
        std::cout << "Result: " << result << std::endl;
    }
    catch (const std::invalid_argument& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
    }

    return 0;
}

RAII-Based Error Handling

Một mẫu mạnh mẽ khác: giữ lỗi của một thao tác trong object, rồi kiểm tra khi cần:

raii_error_handling.cpp
#include <iostream>
#include <optional>
#include <string>

class FileResult {
private:
    std::optional<std::string> content;
    std::string errorMessage;
    bool hasError = false;

public:
    void readFile(const std::string& filename) {
        // Thay vì ném exception, lưu error
        std::ifstream file(filename);
        if (!file.is_open()) {
            hasError = true;
            errorMessage = "Cannot open file: " + filename;
            return;
        }
        std::string data((std::istreambuf_iterator<char>(file)),
                         std::istreambuf_iterator<char>());
        content = data;
    }

    bool isOk() const { return !hasError; }
    const std::string& getError() const { return errorMessage; }
    const std::string& getValue() const { return content.value(); }
};

int main() {
    FileResult result;
    result.readFile("config.txt");

    if (result.isOk()) {
        std::cout << "File content: " << result.getValue() << std::endl;
    } else {
        std::cerr << "Error: " << result.getError() << std::endl;
    }

    return 0;
}

7. Performance & Best Practices

Hiệu năng của Exception Handling: Exceptions có overhead tính toán khi được ném, nhưng chi phí khi không có exception xảy ra là rất thấp (zero-cost model). Điều này có nghĩa:

  • Đặt exception handling ở nơi mà lỗi thực sự có thể xảy ra (VD: I/O, network, validation).
  • Không sử dụng exceptions cho control flow bình thường (VD: dùng exception để kiểm tra điều kiện loop).
  • Trong code hiệu năng cực cao (inner loops, game engines), ưu tiên error codes hoặc optional.

Best Practices Cho Exception Handling

1. Throw by value, catch by const reference:

// ĐÚNG
try {
    throw std::invalid_argument("Invalid input");
}
catch (const std::invalid_argument& ex) {  // catch by const ref
    // ...
}

// SAI
try {
    throw std::invalid_argument("Invalid input");
}
catch (std::invalid_argument ex) {  // Sao chép không cần thiết!
    // ...
}
              

2. Đặc biệt cẩn thận với destructor:

class Resource {
public:
    ~Resource() noexcept {  // Destructor PHẢI là noexcept!
        // Không được ném exception
        cleanup();
    }

private:
    void cleanup() noexcept {
        // Tất cả cleanup code phải là no-throw
    }
};
              

3. Re-throw exceptions để bảo vệ information:

try {
    riskyOperation();
}
catch (const std::exception& ex) {
    // Xử lý lỗi nếu cần, rồi re-throw
    std::cerr << "Error occurred: " << ex.what() << std::endl;
    throw;  // Re-throw exception gốc
}
              

4. Sử dụng exception specifications (hiện đại):

// C++17+: noexcept specification
void swap(Vector& other) noexcept {  // Guarantee: never throws
    // ...
}

void mayThrow() {  // Có thể ném exception
    // ...
}
              

Câu hỏi ôn tập kiến thức

Khi một exception được ném trong hàm A gọi hàm B gọi hàm C (C ném exception), thứ tự các destructor nào sẽ được gọi?

A. Destructor của A trước, rồi B, rồi C (FIFO).
B. Destructor của C trước, rồi B, rồi A (LIFO/Stack unwinding).
C. Không có destructor nào được gọi khi exception xảy ra.

Tải mã nguồn mẫu bài học

Bạn có thể tải file mã nguồn C++ mẫu hoàn chỉnh của bài học này để tiến hành thực hành trực tiếp trên máy tính cá nhân.

Tải exception_handling.cpp