Danh sách bài viết

Bài 16: Xử lý lỗi với try / except / finally

Học cú pháp try / except / else / finally trong Python: bắt exception đúng loại, lấy thông tin lỗi, raise và re-raise, tạo custom exception, tránh anti-pattern bắt quá rộng, áp dụng cho retry network call và validate input trong code AI / data.

24/05/2026
12 phút đọc
0 lượt xem
1

Mục tiêu bài học

  • Hiểu khái niệm exception và cây phân cấp BaseException → Exception → ....
  • Viết try / except / else / finally đúng vị trí, đúng vai trò.
  • Bắt exception cụ thể, lấy thông tin lỗi qua as e.
  • Dùng raise để phát lỗi, dùng raise trần để re-raise.
  • Tạo custom exception cho domain riêng của project.
  • Nhận diện anti-pattern (bắt bare except:, nuốt lỗi, dùng exception cho control flow thường).
  • Áp dụng vào tình huống AI / data: retry API call, validate input.
2

Vì sao cần xử lý lỗi

Code AI / data hiếm khi chỉ tính toán trên dữ liệu sạch trong RAM. Thường nó phải:

  • Đọc file CSV, JSON, ảnh — file có thể không tồn tại, sai format, encoding lạ.
  • Gọi API model (OpenAI, Anthropic, HuggingFace) — mạng có thể timeout, server trả 429, 500.
  • Load model checkpoint — file có thể corrupt, version mismatch.
  • Parse user input — có thể không phải số, vượt range, rỗng.

Nếu không xử lý lỗi, chương trình sẽ ngừng ngay lần đầu gặp tình huống bất thường. Trong batch job chạy 10 giờ qua 100k record, một KeyError ở record thứ 50k có thể vứt bỏ toàn bộ tiến độ.

Xử lý lỗi tốt cho phép code: bỏ qua record hỏng, retry thao tác có thể thoáng qua (network), báo lỗi rõ ràng cho người dùng thay vì traceback Python thô.

3

Exception trong Python là gì

Trong Python, mỗi lỗi runtime là một object thuộc một class kế thừa từ BaseException. Khi xảy ra lỗi, interpreter tạo object đó và raise nó lên — luồng thực thi nhảy ra khỏi điểm lỗi, đi ngược stack call cho đến khi gặp khối try bắt được, hoặc thoát chương trình.

Phân cấp rút gọn (Python 3.12):

BaseException
 ├── SystemExit          # sys.exit() raise
 ├── KeyboardInterrupt   # Ctrl+C
 ├── GeneratorExit
 └── Exception           # base cho hầu hết lỗi user-level
      ├── ArithmeticError
      │    └── ZeroDivisionError
      ├── LookupError
      │    ├── IndexError
      │    └── KeyError
      ├── ValueError
      ├── TypeError
      ├── OSError
      │    └── FileNotFoundError
      └── ...

Quy ước quan trọng: code ứng dụng chỉ nên bắt class kế thừa Exception, KHÔNG bắt BaseException. Vì BaseException bao gồm cả KeyboardInterruptSystemExit — nếu bắt nó, người dùng nhấn Ctrl+C cũng không thoát được.

4

Cú pháp try / except cơ bản

Khung tối thiểu:

# Đoạn code có thể lỗi đặt trong try
try:
    x = int("abc")           # ValueError
except Exception:
    # Chạy khi try ném ra exception bất kỳ (kế thừa Exception)
    print("Không parse được số")

Khi int("abc") raise, dòng đó dừng giữa chừng, control nhảy vào khối except. Code sau try vẫn tiếp tục chạy bình thường.

Lưu ý: KHÔNG dùng bare except:. Cú pháp except: không tham số bắt cả BaseException — gồm cả Ctrl+C và SystemExit. Đây là anti-pattern phổ biến nhất. Tối thiểu hãy dùng except Exception:.

# SAI - bắt mọi thứ kể cả KeyboardInterrupt
try:
    risky()
except:
    pass

# Đỡ hơn nhưng vẫn quá rộng
try:
    risky()
except Exception:
    pass
5

Bắt loại exception cụ thể

Quy tắc thực hành: chỉ bắt loại exception bạn biết cách xử lý. Mọi loại khác hãy để bubble lên — chúng là bug cần fix, không phải tình huống cần che giấu.

def parse_age(s):
    try:
        return int(s)
    except ValueError:
        # Chỉ bắt khi không parse được số
        return None

Bắt nhiều loại trong một except bằng tuple:

try:
    result = process(data)
except (ValueError, TypeError):
    # Bắt cả ValueError lẫn TypeError, cùng cách xử lý
    result = default_value

Hoặc tách thành nhiều khối except nếu mỗi loại có cách xử lý riêng:

try:
    data = load_config("config.yaml")
except FileNotFoundError:
    # Tạo config mặc định
    data = {"lr": 1e-3, "batch_size": 32}
except ValueError:
    # File tồn tại nhưng nội dung sai
    print("Config sai format, dừng")
    raise

Thứ tự khối except có ý nghĩa: Python kiểm tra từ trên xuống, dừng ở khối khớp đầu tiên. Vì vậy nếu đặt class cha (vd Exception) ở trên, mọi khối phía dưới sẽ không bao giờ chạy.

6

Lấy thông tin exception với as

Cú pháp except ExceptionType as name gán object exception vào biến name để đọc thông tin:

try:
    int("abc")
except ValueError as e:
    print(type(e).__name__)   # ValueError
    print(str(e))             # invalid literal for int() with base 10: 'abc'
    print(e.args)             # ("invalid literal for int() with base 10: 'abc'",)

Với exception có attribute riêng (ví dụ OSError), có thể đọc thêm:

try:
    open("khong-co-file.txt")
except FileNotFoundError as e:
    print(e.errno)     # 2
    print(e.filename)  # 'khong-co-file.txt'

Biến e chỉ tồn tại trong khối except. Sau khối, Python xoá nó để giải phóng reference đến traceback. Nếu cần dùng ngoài, hãy gán sang biến khác.

7

Mệnh đề else

else chạy khi và chỉ khi khối try không raise exception nào. Mục đích: tách phần code có thể lỗi (trong try) khỏi phần code chỉ chạy khi success (trong else).

try:
    f = open("data.csv")
except FileNotFoundError:
    print("File không có")
else:
    # Chỉ chạy khi open() không raise
    content = f.read()
    f.close()
    print(f"Đọc được {len(content)} byte")

Vì sao không gộp content = f.read() vào trong try? Vì nếu gộp, một FileNotFoundError từ chỗ khác (ví dụ file dependency) cũng có thể vô tình rơi vào cùng khối except. else giúp try chỉ giữ đúng dòng có thể raise loại lỗi ta muốn bắt.

8

Mệnh đề finally

finally luôn chạy, bất kể try có raise hay không, có bị bắt hay không, thậm chí cả khi trong tryreturn. Dùng để cleanup: đóng file, đóng DB connection, giải phóng GPU memory, ghi log "đã chạy".

f = None
try:
    f = open("data.csv")
    process(f.read())
except FileNotFoundError:
    print("File không có")
finally:
    # Chạy dù process() có raise hay không
    if f is not None:
        f.close()

Trật tự đầy đủ là try → except → else → finally. Cả 4 đều optional trừ try, nhưng phải có ít nhất except hoặc finally đi kèm.

try:
    x = compute()
except ValueError:
    x = 0
else:
    print("compute() ok")
finally:
    print("dọn dẹp")

Trong code thực tế, với file và connection nên dùng with statement (context manager) — sẽ học ở bài 17. Pattern try / finally + close chỉ cần khi không có context manager phù hợp.

9

raise — chủ động phát exception

Khi hàm phát hiện input không hợp lệ, hoặc state không nên xảy ra, hãy raise exception thay vì trả về giá trị "sentinel" như -1 hay None.

def softmax_temperature(logits, T):
    if T <= 0:
        # Báo lỗi rõ ràng cho caller
        raise ValueError(f"T phải > 0, nhận được {T}")
    # ... tính toán

Quy ước chọn class:

  • ValueError — đúng kiểu, sai giá trị (vd số âm cho cái cần dương).
  • TypeError — sai kiểu (vd truyền string khi cần int).
  • KeyError — key không có trong dict.
  • IndexError — index ngoài range của list / tuple.
  • NotImplementedError — method abstract chưa override.
  • RuntimeError — state không hợp lệ, không khớp loại nào ở trên.

Tránh raise Exception trần — caller sẽ phải bắt rộng và mất ý nghĩa.

10

Re-raise trong except

Đôi khi muốn làm gì đó khi bắt được exception (log, cleanup, increment counter) nhưng vẫn để lỗi bubble lên trên. Dùng raise trần (không đối số) bên trong except:

def load_model(path):
    try:
        model = torch.load(path)
    except FileNotFoundError:
        logger.error(f"Không tìm thấy checkpoint: {path}")
        raise   # Re-raise nguyên exception gốc, giữ traceback
    return model

Còn nếu muốn thay bằng exception khác nhưng giữ context, dùng raise NewException(...) from e:

def load_config(path):
    try:
        with open(path) as f:
            return json.load(f)
    except FileNotFoundError as e:
        # Chuyển sang exception domain-specific
        raise ConfigError(f"Config '{path}' không có") from e

Khi from e được dùng, traceback hiển thị cả lỗi gốc lẫn lỗi mới — debug dễ hơn vì biết nguyên nhân chuỗi.

11

Custom exception

Project lớn nên định nghĩa exception riêng cho domain. Giúp caller phân biệt lỗi của thư viện với lỗi của ứng dụng.

# exceptions.py
class AppError(Exception):
    """Base cho mọi exception của app."""

class ConfigError(AppError):
    """Lỗi liên quan tới file config."""

class ModelLoadError(AppError):
    """Lỗi khi load model checkpoint."""

class APIRateLimitError(AppError):
    """API trả 429."""

Trong code chính:

try:
    run_pipeline()
except ConfigError as e:
    # Lỗi user-fixable, in hướng dẫn
    print(f"Cấu hình sai: {e}")
except ModelLoadError as e:
    print(f"Không load được model: {e}")
except AppError as e:
    # Bắt mọi lỗi domain còn lại
    print(f"App error: {e}")

Custom exception kế thừa Exception, không phải BaseException. Thường chỉ cần một dòng pass hoặc docstring trong body — Python lo phần còn lại.

12

Anti-pattern thường gặp

1. Bắt quá rộng rồi nuốt lỗi.

# SAI - mọi bug đều biến mất, không log
try:
    big_function()
except Exception:
    pass

Lỗi đáng lẽ phải vỡ ngay (vd typo tên biến → NameError) bị nuốt mất, debug rất khó.

2. Dùng exception cho control flow thông thường.

# SAI - dùng exception để check key
try:
    v = d["foo"]
except KeyError:
    v = None

# ĐÚNG - check trực tiếp
v = d.get("foo")

Lưu ý: pattern "EAFP" (Easier to Ask Forgiveness than Permission) của Python chấp nhận dùng try/except trong vài tình huống — nhưng đừng dùng cho thao tác có cách check rẻ tiền (dict.get, "x" in d, hasattr).

3. Log rồi raise — lặp ý.

# DƯ - traceback đã chứa thông tin
try:
    risky()
except Exception as e:
    logger.error(f"Có lỗi: {e}")
    raise

Nếu chỉ log và re-raise, Python sẽ log lỗi 2 lần (một lần từ logger.error, một lần khi exception bubble lên unhandled handler). Hoặc chỉ log + không raise (mất bubble), hoặc dùng logger.exception(...) ở chỗ cuối cùng bắt được.

4. except Exception trên đoạn code dài. Càng nhiều dòng trong try, càng nhiều khả năng bắt nhầm lỗi không liên quan. Giữ try chỉ chứa chính dòng có thể raise.

13

Exception phổ biến trong AI / data

  • FileNotFoundError — file dataset, checkpoint, config không tồn tại.
  • PermissionError — không có quyền đọc / ghi (vd ghi vào /usr/local).
  • UnicodeDecodeError — đọc file CSV với encoding sai.
  • json.JSONDecodeError — parse JSON malformed (response API hỏng).
  • KeyError — truy cập dict / row pandas thiếu cột.
  • IndexError — index list / tensor ngoài range.
  • ValueError — convert sai (int("abc"), np.array với shape không khớp).
  • TypeError — gọi hàm sai kiểu (truyền list vào chỗ cần ndarray).
  • ZeroDivisionError — chia 0 (vd normalize với std = 0).
  • StopIteration — iterator hết phần tử (thường được Python xử lý ngầm).
  • requests.HTTPError / requests.Timeout — gọi API model thất bại.
  • torch.cuda.OutOfMemoryError — GPU hết VRAM khi train.

Mỗi loại có cách xử lý khác nhau: FileNotFoundError có thể tạo default; Timeout nên retry; OutOfMemoryError cần giảm batch size.

14

Use case: retry network call & validate input

14.1. Retry network call. Kết hợp try / except với vòng while (đã học ở bài 9):

import time
import requests

def call_api_with_retry(url, max_retries=3, backoff=2.0):
    """Gọi API, retry tối đa max_retries lần với exponential backoff."""
    for attempt in range(max_retries):
        try:
            r = requests.get(url, timeout=10)
            r.raise_for_status()        # raise HTTPError nếu status >= 400
            return r.json()
        except (requests.Timeout, requests.ConnectionError) as e:
            # Lỗi network thoáng qua → retry
            wait = backoff ** attempt
            print(f"Lần {attempt + 1} lỗi ({e}), chờ {wait}s rồi thử lại")
            time.sleep(wait)
        except requests.HTTPError as e:
            # Lỗi từ server: 4xx do mình sai, 5xx có thể retry
            if 500 <= e.response.status_code < 600 and attempt < max_retries - 1:
                time.sleep(backoff ** attempt)
                continue
            raise   # 4xx hoặc hết lượt retry → để bubble lên
    raise RuntimeError(f"Gọi {url} thất bại sau {max_retries} lần")

Lưu ý: chỉ retry với lỗi có thể tự khỏi (timeout, connection, 5xx). Lỗi 401, 404, 422 retry vô ích — phải fix code / config.

14.2. Validate input. Wrap input() trong vòng lặp tới khi user nhập đúng:

def ask_int(prompt, min_value=None, max_value=None):
    """Hỏi user một số nguyên hợp lệ, retry nếu sai."""
    while True:
        raw = input(prompt)
        try:
            value = int(raw)
        except ValueError:
            print("Phải là số nguyên, thử lại")
            continue
        if min_value is not None and value < min_value:
            print(f"Phải >= {min_value}")
            continue
        if max_value is not None and value > max_value:
            print(f"Phải <= {max_value}")
            continue
        return value

# Sử dụng
age = ask_int("Tuổi: ", min_value=0, max_value=150)
15

Bài tập

Bài 1: Hàm chia an toàn.

Viết hàm safe_divide(a, b):

  • Nếu a hoặc b không phải số (int / float) → raise TypeError với message rõ ràng.
  • Nếu b == 0 → bắt ZeroDivisionError và trả về float('inf') với cảnh báo print.
  • Bình thường trả về a / b.

Bài 2: Parse CSV bỏ qua dòng lỗi.

Cho list dòng CSV (string), mỗi dòng định dạng "name,age". Viết hàm trả về list dict [{"name": ..., "age": ...}, ...]. Dòng nào có age không phải số nguyên thì bỏ qua, đếm số dòng bị bỏ và in cuối hàm.

rows = [
    "alice,30",
    "bob,abc",       # lỗi
    "carol,28",
    "dan,",          # lỗi
    "eve,40",
]

Bài 3: Custom exception.

Định nghĩa InvalidEmailError(Exception). Viết hàm validate_email(s): nếu s không chứa ký tự @ hoặc không có . sau @, raise InvalidEmailError với message giải thích. Gọi hàm trong try / except với 3 input mẫu, in lỗi nếu có.

Bài 4: Retry logic.

Mô phỏng API hay fail bằng hàm flaky_call():

import random

def flaky_call():
    if random.random() < 0.7:
        raise ConnectionError("Tạm thời lỗi")
    return "ok"

Viết wrapper retry(fn, max_attempts=5) dùng try / except ConnectionError để retry. Sau khi hết lượt vẫn lỗi → raise RuntimeError. Test 10 lần và in tỉ lệ thành công.

Bài 5 (mở rộng): cleanup với finally.

Mô phỏng "session": viết hàm open_session() trả về dict {"id": 123, "open": True}, và close_session(s) đặt s["open"] = False. Trong hàm do_work(), mở session, gọi một hàm có thể raise ValueError, dùng finally để đảm bảo session luôn được đóng dù có lỗi hay không.

16

Tóm tắt

  • Exception là object kế thừa BaseException; code ứng dụng chỉ nên bắt lớp con của Exception.
  • Cú pháp đầy đủ: try → except → else → finally. else chạy khi không lỗi, finally luôn chạy.
  • Bắt loại cụ thể (ValueError, FileNotFoundError...), tránh bare except:except Exception: trên đoạn dài.
  • except ... as e để đọc message, e.args, các attribute riêng (e.errno, e.filename...).
  • raise ValueError("...") để chủ động báo lỗi; raise trần để re-raise giữ traceback; raise X from e để chuyển sang exception khác kèm context.
  • Custom exception kế thừa Exception, giúp tách lỗi domain với lỗi thư viện.
  • Anti-pattern: nuốt lỗi, dùng exception thay cho check thông thường, log + raise lặp ý, try bao quá nhiều dòng.
  • Use case AI / data: retry network call có backoff cho lỗi thoáng qua; validate input qua loop + try/except.

Bài tiếp theo học open() và context manager — cách đọc / ghi file text đúng chuẩn, không cần thủ công try / finally + close().