← Lập trình C nâng cao

Bài 14 · Nâng cao · 22 phút· Cập nhật 11/06/2026

Xử lý lỗi kiểu C

Biên soạn bởi Nguyễn Anh Tuấn

Xử lý lỗi (error handling) trong C: báo lỗi bằng giá trị trả về & errno, hợp đồng hàm enum mã lỗi, dọn tài nguyên với goto cleanup - viết C không nuốt lỗi.

C không có exception, không có try/catch - toàn bộ xử lý lỗi (error handling) trong C đi qua đúng một kênh: giá trị trả về. Bạn đã gặp quy ước này suốt khoá mà có thể chưa gọi tên:

HàmTín hiệu lỗiĐã gặp ở
malloc · fopenNULLBài 7 · Bài 13
fgetc · getcharEOF (số âm)khoá cơ bản
scanf · fscanfsố mục đọc được < mong đợikhoá cơ bản
main / cả chương trìnhexit code ≠ 0mọi bài
  • Lỗi là MỘT GIÁ TRỊ - không có bộ máy ngầm nào lan truyền nó giúp bạn.
  • Hệ quả: trình biên dịch KHÔNG ép kiểm lỗi; bỏ qua giá trị trả về là hợp lệ và là nguồn bug kinh điển.
  • Kỷ luật nền: gọi hàm có thể thất bại → kiểm ngay tại chỗ.

fopen trả NULL chỉ nói "hỏng" - không nói hỏng vì gì. Lý do nằm trong errno: biến số nguyên mà các hàm chuẩn đặt mã lỗi vào trước khi trả về thất bại. Hai trợ thủ dịch nó ra chữ: perrorstrerror:

vi-sao.c - mở một file không tồn tại

#include <stdio.h>
#include <errno.h>
#include <string.h>

int main(void) {
    FILE *f = fopen("khong-co-that.txt", "r");
    if (f == NULL) {
        perror("fopen");                       /* in: ten + ly do loi */
        printf("errno = %d\n", errno);
        printf("nghia la: %s\n", strerror(errno));
        return 1;
    }
    fclose(f);
    return 0;
}

Kết quả khi chạy

fopen: No such file or directory
errno = 2
nghia la: No such file or directory
  • errno chỉ đáng tin NGAY SAU lời gọi vừa thất bại - gọi thêm hàm khác là nó có thể bị ghi đè.
  • perror("ten") in "ten: lý do" ra stderr - stderr KHÔNG buffer (nhớ bài 13) nên thông điệp lỗi luôn thoát ra được, kể cả khi chương trình sắp sập.
  • Các hàm thành công KHÔNG đặt errno = 0 giúp bạn - muốn dùng kiểu dò, tự gán errno = 0 trước khi gọi.

Đến lượt hàm của bạn phải báo lỗi tử tế. Khuôn phổ biến nhất: giá trị trả về dành cho mã lỗi (một enum đặt tên rõ), còn kết quả thật đi ra qua out-parameter (truyền địa chỉ - Bài 2):

hop-dong.c - một hàm, hai đường lỗi đặt tên rõ ràng (output bên dưới là khi CHƯA có diem.txt)

#include <stdio.h>

/* Hop dong: tra ve ma loi; ket qua di ra qua out-parameter */
enum DocDiem {
    DOC_OK = 0,
    DOC_KHONG_MO_DUOC,
    DOC_HONG_DINH_DANG
};

enum DocDiem doc_diem_dau(const char *ten_tep, double *ra_diem) {
    FILE *f = fopen(ten_tep, "r");
    if (f == NULL) return DOC_KHONG_MO_DUOC;

    int doc_duoc = fscanf(f, "%*s %lf", ra_diem);  /* bo qua ten mon, lay diem */
    fclose(f);

    if (doc_duoc != 1) return DOC_HONG_DINH_DANG;
    return DOC_OK;
}

int main(void) {
    double diem;
    switch (doc_diem_dau("diem.txt", &diem)) {
        case DOC_OK:              printf("Diem dau tien: %.1f\n", diem); break;
        case DOC_KHONG_MO_DUOC:   printf("Khong mo duoc diem.txt\n");    return 1;
        case DOC_HONG_DINH_DANG:  printf("File sai dinh dang\n");        return 1;
    }
    return 0;
}

Kết quả khi chạy

Khong mo duoc diem.txt
  • Mã lỗi enum tự ghi tài liệu: người đọc switch là thấy đủ các tình huống - và quên một case thì -Wall nhắc.
  • Quy ước 0 = thành công (như exit code) giúp viết if (doc_diem_dau(…) != DOC_OK) gọn gàng.
  • Có diem.txt hợp lệ, chương trình in "Diem dau tien: 9.5" - cả hai đường đều đã chạy kiểm thật.

Khó nhất của xử lý lỗi C không phải báo lỗi - mà là dọn dẹp. Hàm xin 3 tài nguyên, lỗi ở bước 3: hai thứ đã xin phải được trả lại, không thì rò rỉ. Chạy thử cả hai phong cách, ép lỗi từng chỗ mà xem:

·
0/3 bước
  1. bấm "Bước ▶" để chạy hàm…
Tài nguyên đang giữ
(không giữ gì)

Ba bước xin lần lượt: fopen("log.txt", "w") → malloc(1024) → fopen("du-lieu.txt", "r")

Khuôn goto cleanup bằng C thật - lỗi ở bước 3 nhưng không rò gì:

don-dep.c - xin xuôi, dọn ngược, MỘT lối ra (chạy kiểm thật: file du lieu không tồn tại)

#include <stdio.h>
#include <stdlib.h>

int xu_ly(void) {
    int ket_qua = -1;          /* bi quan: coi nhu that bai */
    FILE *nhat_ky = NULL;
    char *bo_dem = NULL;

    nhat_ky = fopen("log.txt", "w");
    if (nhat_ky == NULL) goto don_dep;

    bo_dem = malloc(1024);
    if (bo_dem == NULL) goto don_dep;

    FILE *du_lieu = fopen("khong-ton-tai.txt", "r");
    if (du_lieu == NULL) {
        perror("Khong mo duoc du lieu");
        goto don_dep;          /* loi giua chung - van don du */
    }
    /* ... lam viec voi ca ba tai nguyen ... */
    fclose(du_lieu);
    ket_qua = 0;

don_dep:                       /* don MOT CHO, nguoc thu tu xin */
    free(bo_dem);              /* free(NULL) hop le - khoi can if */
    if (nhat_ky) fclose(nhat_ky);
    return ket_qua;
}

int main(void) {
    if (xu_ly() != 0) {
        printf("xu_ly that bai - nhung khong ro ri tai nguyen nao\n");
        return 1;
    }
    printf("xong\n");
    return 0;
}

Kết quả khi chạy

Khong mo duoc du lieu: No such file or directory
xu_ly that bai - nhung khong ro ri tai nguyen nao
  • Bộ ba của khuôn: khởi tạo con trỏ = NULL · kết quả "bi quan" từ đầu · một nhãn don_dep cuối hàm dọn NGƯỢC thứ tự xin.
  • free(NULL) vô hại (chuẩn bảo đảm) nên nhãn dọn không cần if cho malloc; fclose thì cần chắn if (f).
  • goto ở đây chỉ nhảy XUÔI tới một nhãn duy nhất - luồng vẫn thẳng, khác hẳn goto chằng chịt bị cấm.
  • Gọi hàm có thể thất bại → kiểm NGAY; cần lý do → errno + perror/strerror.
  • Hàm của bạn: enum mã lỗi trả về (0 = OK), kết quả qua out-parameter, hợp đồng ghi rõ trong header.
  • Nhiều tài nguyên → khuôn goto cleanup; đường lỗi cũng là đường đi của chương trình, viết tử tế như happy path.
  • assert dành cho lỗi LẬP TRÌNH (tắt ở bản release); lỗi runtime chính đáng thì xử lý bằng mã lỗi.

Sẽ gặp lại

Đường lỗi viết ra mà không ai chạy thử thì vẫn là đường mòn cỏ mọc. Bài Unit test (cuối khoá) sẽ kiểm đúng những đường lỗi này - pop khi rỗng, malloc thất bại - bằng test tự động.

Bài tiếp theo

Còn một mảnh cuối của bộ đồ nghề C hiện đại: macro dạng hàm, inline và _Generic - viết một lần, chạy cho nhiều kiểu, mà không trả giá lúc runtime.

Câu hỏi thường gặp

Triết lý C: không có chi phí ẩn. Exception cần bộ máy lan truyền lỗi ngầm xuyên các tầng gọi hàm - tốn và khó đoán. C chọn cách rẻ nhất: lỗi là MỘT GIÁ TRỊ trả về như mọi giá trị khác, người gọi tự quyết. Đổi lại, trình biên dịch không ép bạn kiểm - kỷ luật nằm ở người viết.

errno hiện đại là biến theo-từng-luồng (mỗi luồng một bản), nên đa luồng không giẫm nhau. Điều cần nhớ hơn: errno chỉ đáng tin NGAY SAU lời gọi vừa thất bại - gọi thêm hàm khác (kể cả printf) là nó có thể bị ghi đè.

Hàm trả CON TRỎ thì NULL là tín hiệu lỗi tự nhiên (malloc, fopen). Hàm cần phân biệt NHIỀU loại lỗi thì trả enum mã lỗi và đẩy kết quả qua out-parameter. Hàm kiểu "có/không" trả bool. Quan trọng nhất là NHẤT QUÁN trong cùng một module - người dùng đọc một hàm là đoán được cả họ.

Cái bị cấm là goto NHẢY LUNG TUNG (ngược lên trên, chéo qua khối) làm luồng chạy thành mì sợi. goto cleanup thì ngược lại: chỉ nhảy XUÔI, tới đúng MỘT nhãn cuối hàm - luồng vẫn thẳng. Kernel Linux dùng khuôn này dày đặc; chính chương Program failure của Modern C cũng dạy nó.

Để nhãn don_dep dọn được AN TOÀN bất kể lỗi xảy ra ở bước nào: free(NULL) là vô hại (chuẩn bảo đảm), fclose thì cần if (f) chắn. Còn kết quả khởi tạo = thất bại nghĩa là chỉ MỘT chỗ duy nhất gán thành công - quên gán thì hàm "thà báo lỗi oan còn hơn báo thành công ẩu".

Không - assert dành cho LỖI LẬP TRÌNH (điều "không thể xảy ra" nếu code đúng), và biến mất khi build release với -DNDEBUG. Lỗi RUNTIME chính đáng (file không tồn tại, hết bộ nhớ, người dùng gõ bậy) phải xử lý bằng mã lỗi như bài này - chúng xảy ra cả trong bản phát hành.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

C không có exception - một hàm thư viện chuẩn báo thất bại bằng cách nào?

  1. 1

    Đọc bản đồ lỗi

    Tra man fopen (hoặc cppreference) xem mục RETURN VALUE và ERRORS. fopen có thể thất bại vì những lý do nào ngoài "file không tồn tại"?

    Hoàn thành khi: Kể được ít nhất 2 lý do khác (hết quyền truy cập, quá nhiều file đang mở…) - và thấy quy ước NULL + errno ngay trong tài liệu chuẩn.

  2. 2

    errno tự tay

    Chạy chương trình Bước 2. Rồi thử gây một lỗi KHÁC: fopen một file có thật nhưng mở bằng quyền không hợp lệ (chmod 000 file rồi mở "r").

    Hoàn thành khi: errno đổi từ 2 (ENOENT) sang 13 (EACCES) - perror đổi câu tương ứng.

  3. 3

    Phá hợp đồng

    Với chương trình Bước 3: tạo diem.txt có nội dung "xyz abc" (không có số) và chạy.

    Hoàn thành khi: Hàm trả DOC_HONG_DINH_DANG, main in "File sai dinh dang" - đường lỗi thứ hai hoạt động đúng hợp đồng.

  4. 4

    Săn rò rỉ trong công cụ

    Trong công cụ Bước 4: với kiểu "return ngay khi lỗi", thử đủ 4 vị trí lỗi và ghi lại cái gì rò rỉ ở mỗi vị trí.

    Hoàn thành khi: Không lỗi & lỗi bước 1: sạch; lỗi bước 2: rò nhat_ky; lỗi bước 3: rò nhat_ky + bo_dem. Đổi sang goto don_dep: sạch cả 4.

  5. 5

    Viết lại bằng goto cleanup

    Lấy một hàm của app Todo (bài Tệp & luồng dữ liệu) có mở file + malloc, viết lại theo khuôn: khởi tạo NULL, kết quả bi quan, một nhãn don_dep.

    Hoàn thành khi: Mọi đường lỗi đều qua don_dep; chạy thử cả đường thành công lẫn đường lỗi (xoá file đi) - không đường nào quên fclose/free.

  6. 6

    Đối chiếu thực chiến

    Mở mã nguồn một dự án C thật trên mạng (gợi ý: tìm "goto out" hoặc "goto err" trong kernel Linux trên GitHub) và đọc một hàm dùng khuôn này.

    Hoàn thành khi: Chỉ ra được: các bước xin tài nguyên, các goto, và thứ tự dọn ngược tại nhãn.