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àm | Tín hiệu lỗi | Đã gặp ở |
|---|---|---|
| malloc · fopen | NULL | Bài 7 · Bài 13 |
| fgetc · getchar | EOF (số âm) | khoá cơ bản |
| scanf · fscanf | số mục đọc được < mong đợi | khoá cơ bản |
| main / cả chương trình | exit code ≠ 0 | mọ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ữ: perror và strerror:
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:
- bấm "Bước ▶" để chạy hàm…
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
Bài tiếp theo
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ó.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
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
Đọc bản đồ lỗi
Tra
man fopen(hoặc cppreference) xem mục RETURN VALUE và ERRORS.fopencó 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+errnongay trong tài liệu chuẩn. - 2
errno tự tay
Chạy chương trình Bước 2. Rồi thử gây một lỗi KHÁC:
fopenmột file có thật nhưng mở bằng quyền không hợp lệ (chmod 000file 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
Phá hợp đồng
Với chương trình Bước 3: tạo
diem.txtcó nội dung "xyz abc" (không có số) và chạy.Hoàn thành khi: Hàm trả
DOC_HONG_DINH_DANG,mainin "File sai dinh dang" - đường lỗi thứ hai hoạt động đúng hợp đồng. - 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 sanggoto don_dep: sạch cả 4. - 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ạoNULL, kết quả bi quan, một nhãndon_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ênfclose/free. - 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.