# Ngoại lệ (exception) trong Python: try, except, else, finally, raise và ngoại lệ tự định nghĩa

URL: https://skillaria.top/vi/guides/python/xu-ly-ngoai-le-python

Ngoại lệ (exception) trong Python là một đối tượng báo lỗi, làm dừng luồng chạy bình thường của code cho đến khi có khối try/except bắt lấy nó; else chạy khi không có lỗi, còn finally luôn chạy. Có thể tự gây lỗi bằng lệnh raise, ví dụ raise ValueError("số tiền âm"), còn ngoại lệ riêng được tạo bằng cách kế thừa class Exception.

Ngoại lệ (exception) trong Python là một đối tượng báo hiệu lỗi và làm gián đoạn luồng chạy bình thường của chương trình. Nếu không ai bắt nó, Python in traceback và dừng chương trình. Ngoại lệ được bắt bằng cấu trúc `try/except`, còn được gây ra bằng lệnh `raise`. Dưới đây là cách hoạt động, các lỗi phổ biến nhất, và cách xử lý lỗi mà không biến nó thành việc che giấu lỗi.

## Ngoại lệ trong Python là gì

Khi một thao tác không thể thực hiện — chia cho 0, truy cập một khóa không tồn tại trong dict, chuyển một chữ thành số — Python tạo một đối tượng ngoại lệ và «ném» nó ra. Hàm hiện tại dừng lại, ngoại lệ được chuyển lên theo chuỗi lời gọi, cho đến khi gặp trình xử lý phù hợp. Nếu không có, chương trình sẽ bị dừng.

```
>>> 10 / 0
Traceback (most recent call last):
  File "", line 1, in
ZeroDivisionError: division by zero
```

Nhân tiện trả lời một câu hỏi thường gặp: khi chia cho 0 trong Python, sẽ có ngoại lệ `ZeroDivisionError`. Điều này áp dụng cho cả `/`, `//` và `%`.

## Cách đọc traceback

Traceback là báo cáo cho biết chương trình đã đi đến lỗi như thế nào. Nên đọc từ dưới lên. Dòng cuối là quan trọng nhất: kiểu ngoại lệ và thông báo. Phía trên là dòng code nơi lỗi xảy ra, và cao hơn nữa là nơi đã gọi nó, cho đến điểm bắt đầu chương trình.

```
Traceback (most recent call last):
  File "chi_tieu.py", line 9, in
    total = parse_amount(row)
  File "chi_tieu.py", line 4, in parse_amount
    return float(raw.strip())
           ~~~~~^^^^^^^^^^^^^
ValueError: could not convert string to float: 'mười'
```

Từ Python 3.11, dưới dòng code có các dấu `~~~^^^` (PEP 657), chỉ ra chính xác biểu thức nào trong dòng đã gây lỗi. Điều này đặc biệt hữu ích khi một dòng có nhiều lời gọi liên tiếp.

### Gợi ý «Did you mean»

Từ Python 3.10, khi gõ sai tên, thông báo lỗi đề xuất tên gần đúng. Trong Python 3.15, các gợi ý thông minh hơn: trình thông dịch nhận ra thói quen từ các ngôn ngữ khác. Gọi `list.push` sẽ gợi ý `append`, còn `"cà phê".toUpperCase()` sẽ gợi ý phương thức `upper`. Gợi ý cũng xét cả các thuộc tính lồng nhau: nếu dict nằm trong thuộc tính `totals` của một object và bạn gọi `items()` trực tiếp trên object đó, lỗi sẽ gợi ý `.totals.items`. Hàm `delattr()` cũng đã có gợi ý tên gần giống.

## Các kiểu ngoại lệ trong Python

Có hàng chục ngoại lệ tích hợp, nhưng khi mới học bạn sẽ gặp chủ yếu những loại sau:

| Ngoại lệ | Khi nào xuất hiện | Ví dụ |
| --- | --- | --- |

| `NameError` | Tên chưa được định nghĩa, thường do gõ sai | `print(tong_tien)` |

| `TypeError` | Phép toán không phù hợp với kiểu này | `"5" + 3` |

| `ValueError` | Kiểu đúng, nhưng giá trị không hợp lệ | `int("mười")` |

| `KeyError` | Không có khóa đó trong dict | `d["thieu"]` |

| `IndexError` | Chỉ số nằm ngoài list | `[1, 2][5]` |

| `AttributeError` | Object không có phương thức hoặc thuộc tính đó | `[].push(1)` |

| `ZeroDivisionError` | Chia cho 0 | `1 / 0` |

| `FileNotFoundError` | Không tìm thấy tệp | `open("khong_co.txt")` |

Riêng `SyntaxError` (và trường hợp đặc biệt `IndentationError`) xuất hiện ngay khi đọc tệp, trước khi chạy code, nên không thể bắt nó bằng `try` trong cùng tệp đó.

### Cây phân cấp ngoại lệ

Ngoại lệ là các class và được tổ chức thành cây phân cấp. Ở gốc là `BaseException`. Từ đó kế thừa `Exception` (hầu hết các lỗi «thông thường») và một số lỗi hệ thống: `KeyboardInterrupt`, `SystemExit`, `GeneratorExit`.

```
BaseException
 ├── KeyboardInterrupt
 ├── SystemExit
 └── Exception
      ├── ArithmeticError
      │    └── ZeroDivisionError
      ├── LookupError
      │    ├── KeyError
      │    └── IndexError
      ├── OSError
      │    └── FileNotFoundError
      ├── TypeError
      └── ValueError
```

Cây phân cấp quan trọng khi bắt lỗi: `except LookupError` bắt được cả `KeyError` lẫn `IndexError`. Còn `KeyboardInterrupt` không phải lỗi trong code: nó báo rằng người dùng đã nhấn Ctrl+C. Nó không kế thừa từ `Exception` một cách có chủ đích, để `except Exception` không chặn được việc dừng chương trình.

## Xử lý ngoại lệ: try/except/else/finally

Đoạn code có thể gây lỗi được đặt trong khối `try`. Trong khối `except`, bạn chỉ rõ kiểu lỗi và việc cần làm khi gặp nó. Chính `except` là khối được Python dùng để xử lý ngoại lệ.

```
def parse_amount(raw):
    try:
        value = float(raw)
    except ValueError as e:
        print(f"Bỏ qua dòng: {e}")
        return None
    else:
        return value          # chạy khi không có lỗi
    finally:
        print("đã phân tích xong")   # luôn chạy

print(parse_amount("250"))
```

Thứ tự in ra ở đây là: đầu tiên «đã phân tích xong», sau đó là `250.0`. `return` trong khối `else` ghi nhớ giá trị, sau đó Python chắc chắn chạy `finally` rồi mới trả kết quả. Vì vậy `finally` đáng tin cậy để dọn dẹp: đóng tệp, giải phóng kết nối.

Nếu viết một `return` riêng trong `finally`, nó sẽ ghi đè kết quả trước đó và thậm chí «nuốt» ngoại lệ. Từ Python 3.14, code kiểu này sinh ra `SyntaxWarning` (PEP 765) — đừng viết `return` trong `finally`.

### Bắt nhiều ngoại lệ cùng lúc

Có thể viết nhiều khối `except` cho các kiểu khác nhau, hoặc liệt kê các kiểu trong một tuple:

```
try:
    amount = int(data["amount"])
except KeyError:
    amount = 0
except (ValueError, TypeError) as e:
    print("Số tiền không hợp lệ:", e)
    amount = 0
```

Các khối được kiểm tra từ trên xuống, khối khớp đầu tiên được thực thi, vì vậy các kiểu cụ thể nên đặt lên trên các kiểu chung. Từ Python 3.14 (PEP 758), có thể liệt kê nhiều kiểu mà không cần ngoặc đơn — `except ValueError, TypeError:` — nhưng khi có `as` thì vẫn bắt buộc phải có ngoặc.

### Cách in thông báo lỗi và lấy kiểu lỗi

Đối tượng ngoại lệ có sẵn qua `as e`. `str(e)` cho thông báo, `type(e).__name__` cho tên kiểu:

```
try:
    int("mười")
except ValueError as e:
    print(type(e).__name__)   # ValueError
    print(e)                  # invalid literal for int() with base 10: 'mười'
```

## Module traceback: toàn bộ nội dung lỗi

Đôi khi cần bắt lỗi nhưng vẫn giữ nguyên toàn bộ traceback — để ghi log hoặc báo cáo. Module `traceback` phục vụ việc này:

```
import traceback

try:
    1 / 0
except ZeroDivisionError:
    traceback.print_exc()             # in traceback ra stderr
    text = traceback.format_exc()     # cùng nội dung dưới dạng chuỗi
```

Nếu ghi log bằng module `logging`, cách đơn giản hơn là gọi `logging.exception("không tính được tổng")` bên trong `except` — traceback sẽ tự động được thêm vào thông báo.

## Lệnh raise trong Python: gây ngoại lệ

Lệnh `raise` ném một ngoại lệ bằng tay. Nhờ đó hàm cho biết nó không thể xử lý dữ liệu như vậy:

```
def add_expense(amount):
    if amount < 0:
        raise ValueError(f"Số tiền không thể âm: {amount}")
    ...

add_expense(-5)
# ValueError: Số tiền không thể âm: -5
```

`raise` trong Python là một lệnh chứ không phải hàm, vì vậy sau từ `raise` không cần dấu ngoặc: các đối số được truyền vào hàm khởi tạo của ngoại lệ bên trong dấu ngoặc. Chọn kiểu theo ý nghĩa: giá trị sai là `ValueError`, kiểu sai là `TypeError`.

### raise không có đối số và raise from

Bên trong `except`, một `raise` trơn sẽ chuyển tiếp ngoại lệ hiện tại mà không thay đổi gì. Điều này hữu ích khi bạn muốn ghi log nhưng vẫn để code gọi bên ngoài xử lý tiếp.

Nếu thay một ngoại lệ bằng ngoại lệ khác, hãy dùng `raise ... from e` — lỗi gốc được giữ lại trong chuỗi, và traceback thể hiện cả hai:

```
def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError as e:
        raise RuntimeError(f"Không có tệp cấu hình {path}") from e
```

### add_note: thêm ngữ cảnh cho lỗi

Từ Python 3.11 (PEP 678), có thể thêm ghi chú vào ngoại lệ bằng phương thức `add_note()` rồi tiếp tục ném nó. Kiểu lỗi không đổi, còn ghi chú được in ngay dưới thông báo:

```
def parse(raw, line_no):
    try:
        return float(raw)
    except ValueError as e:
        e.add_note(f"tệp chi_tieu.csv, dòng {line_no}")
        raise

parse("mười", 17)
# ValueError: could not convert string to float: 'mười'
# tệp chi_tieu.csv, dòng 17
```

## Cách tạo ngoại lệ riêng trong Python

Ngoại lệ riêng cần cho các lỗi thuộc nghiệp vụ: vượt ngân sách, đơn hàng đã thanh toán, người dùng bị khóa. Chỉ cần một class kế thừa `Exception`:

```
class BudgetExceeded(Exception):
    def __init__(self, category, limit, spent):
        super().__init__(f"{category}: hạn mức {limit}, đã chi {spent}")
        self.category = category
        self.limit = limit
        self.spent = spent

def check_budget(category, limit, spent):
    if spent > limit:
        raise BudgetExceeded(category, limit, spent)

try:
    check_budget("Ăn uống", 10000, 12500)
except BudgetExceeded as e:
    print("Cảnh báo:", e)   # Cảnh báo: Ăn uống: hạn mức 10000, đã chi 12500
```

Bây giờ code gọi bắt đúng lỗi vượt ngân sách và không nhầm nó với lỗi gõ sai. Trong dự án lớn, nên tạo một class cơ sở chung (`class TrackerError(Exception)`) và cho mọi ngoại lệ riêng kế thừa từ đó — khi ấy có thể bắt tất cả bằng một `except TrackerError`. Hãy kế thừa từ `Exception`, không phải từ `BaseException`.

## Nhóm ngoại lệ và except*

Đôi khi có nhiều lỗi cùng lúc — ví dụ khi kiểm tra cả một tệp, hoặc trong các tác vụ song song với `asyncio.TaskGroup`. Từ Python 3.11, chúng có thể được gom lại thành `ExceptionGroup`, và xử lý theo kiểu bằng `except*`:

```
def check_all(rows):
    errors = []
    for r in rows:
        try:
            float(r)
        except ValueError as e:
            errors.append(e)
    if errors:
        raise ExceptionGroup("lỗi trong tệp", errors)

try:
    check_all(["1", "a", "b"])
except* ValueError as eg:
    print(f"Số dòng sai: {len(eg.exceptions)}")   # Số dòng sai: 2
```

## Python try except bắt tất cả lỗi: vì sao không nên

Thói quen tệ nhất là bắt mọi thứ: `except: pass`. Chương trình «không còn đổ lỗi», nhưng thực tế là không còn báo cáo vấn đề nữa. `except` trơn bắt cả `KeyboardInterrupt` và `SystemExit`, nên không thể dừng chương trình bằng Ctrl+C. `except Exception: pass` đỡ hơn một chút, nhưng vẫn nuốt im lặng các lỗi gõ sai tên và lỗi logic.

Cách xử lý ngoại lệ đúng trong Python:

- chỉ bắt kiểu lỗi mà bạn biết xử lý: `except ValueError`, không bắt tất cả;
- giữ khối `try` ngắn — chỉ những dòng có thể gây lỗi đúng kiểu đó;
- nếu bắt rộng (`except Exception`) ở tầng cao nhất của chương trình, nhất định phải ghi log traceback;
- đừng dùng ngoại lệ để rẽ nhánh thông thường khi kiểm tra đơn giản là đủ: dùng `dict.get()` thay vì `try/except KeyError` trong các trường hợp đơn giản;
- với tài nguyên, ưu tiên `with` — nó tự đóng tệp ngay cả khi có lỗi.

## Tổng kết

Ngoại lệ trong Python là cơ chế bình thường để báo lỗi, không phải lý do để giấu lỗi. Đọc traceback từ dưới lên, bắt các kiểu cụ thể trong `try/except`, dùng `else` cho code «khi mọi thứ thành công» và `finally` cho việc dọn dẹp. Lệnh raise cho phép bạn tự báo lỗi, `raise from` và `add_note` giữ được ngữ cảnh, còn ngoại lệ riêng kế thừa từ `Exception`. Chủ đề này được thực hành trong khóa học Python: trình theo dõi chi tiêu không còn sập vì một dòng CSV sai, và việc vượt ngân sách được biểu diễn bằng một ngoại lệ riêng.

## FAQ

**Lỗi và ngoại lệ trong Python khác nhau thế nào?**

Trong lời nói thường ngày chúng là một. Về kỹ thuật, SyntaxError được phát hiện ngay khi đọc code, còn các lỗi khác xảy ra khi chạy dưới dạng đối tượng ngoại lệ có thể bắt được.

**Sau khi bắt ngoại lệ, có thể tiếp tục từ đúng dòng bị lỗi không?**

Không. Sau khi bắt, chương trình tiếp tục ở phía sau khối try/except, không phải ở dòng gây lỗi. Nếu cần xử lý từng dòng của tệp, hãy đặt try bên trong vòng lặp.

**Chuyện gì xảy ra nếu ngoại lệ phát sinh bên trong except?**

Ngoại lệ mới được ném tiếp, còn ngoại lệ gốc được giữ trong thuộc tính __context__. Traceback sẽ hiện câu During handling of the above exception, another exception occurred.

**Làm sao kiểm tra trong test rằng hàm ném ngoại lệ?**

Trong pytest dùng with pytest.raises(ValueError):, trong unittest dùng self.assertRaises(ValueError). Test chỉ đạt khi bên trong khối có ngoại lệ đúng kiểu được ném ra.

**assert là gì và khác raise ở đâu?**

assert điều_kiện, "thông báo" ném AssertionError khi điều kiện sai. Đây là công cụ kiểm tra trong lúc phát triển: khi chạy với cờ -O, các assert bị tắt, nên kiểm tra dữ liệu người dùng cần dùng if và raise.


---
Source: Skillaria.Top (https://skillaria.top)
