Courses
Khi mã Python gặp vấn đề lúc chạy, nó thường phát sinh một exception. Nếu không được xử lý, exception sẽ làm chương trình của bạn sập. Tuy nhiên, với các khối try-except, bạn có thể bắt chúng, xử lý êm đẹp và giữ cho ứng dụng tiếp tục chạy.
Bài hướng dẫn này không nói về những điều cơ bản của exception, chúng tôi đã đề cập trong Hướng dẫn Xử lý Exception & Lỗi trong Python. Thay vào đó, ở đây chúng ta sẽ đi sâu vào try-except trong thực tiễn: cách cấu trúc các khối, tránh lỗi thường gặp, và áp dụng các thực tiễn tốt nhất giúp mã của bạn đáng tin cậy hơn trong các tình huống thực tế.
Kết thúc bài, bạn sẽ hiểu không chỉ try-except hoạt động thế nào, mà còn cách dùng nó theo phong cách “Pythonic”; viết mã xử lý lỗi rõ ràng, dễ bảo trì và sẵn sàng cho sản xuất.
Nếu bạn vẫn đang trên hành trình học Python, tôi khuyên bạn nên xem lộ trình kỹ năng Python Programming Fundamentals, sẽ giúp bạn xây dựng mọi kỹ năng thiết yếu.
Vì sao tập trung vào try-except?
Xử lý lỗi trong Python về cốt lõi là thiết kế chương trình có thể xử lý những điều bất ngờ. Và dù có nhiều công cụ để xử lý lỗi, try-except là xương sống trong cách tiếp cận của Python.
Vì sao nó lại quan trọng đến vậy?
- Nó phản ánh triết lý của Python. Trong Python, phong cách phổ biến là EAFP; Dễ xin tha thứ hơn là xin phép. Thay vì kiểm tra mọi thứ trước (tệp này có tồn tại không? đầu vào có hợp lệ không? máy chủ có sẵn không?), bạn cứ thử thực hiện. Nếu thất bại, bạn bắt exception và xử lý. Cách này dẫn đến mã ngắn hơn, sạch hơn.
- Nó giữ cho mã chạy được trong môi trường thực tế. Đầu vào người dùng sẽ lộn xộn, tệp sẽ bị thiếu, API sẽ lỗi và mạng sẽ rớt. Với
try-except, bạn có thể viết mã không sụp đổ ngay từ sự cố đầu tiên mà thay vào đó phản ứng một cách thông minh. - Đủ linh hoạt cho cả người mới và chuyên gia. Người mới có thể dùng
try-exceptđể bắtValueErrorkhi chuyển đổi đầu vào sang số nguyên. Hệ thống sản xuất có thể dùng nó để ghi log lỗi, thử lại thao tác thất bại, hoặc raise các exception tùy chỉnh giúp gỡ lỗi dễ hơn.
Đó là lý do bài viết này tập trung vào try-except. Thành thạo nó là khác biệt giữa việc viết các script chỉ chạy trong điều kiện hoàn hảo và viết phần mềm vững chắc, dễ bảo trì và sẵn sàng cho sản xuất.
Giải phẫu một khối Try‑Except
Ở mức đơn giản nhất:
try:
risky_thing()
except SomeError:
handle_it()
Khi Python gặp lỗi bên trong khối try, nó sẽ nhảy đến khối except phù hợp thay vì làm chương trình sập.
Bạn cũng có thể mở rộng mẫu này với các mệnh đề else và finally:
try:
do_something()
except ValueError:
print("Bad value!")
else:
print("All good.")
finally:
clean_up()
Đây là một ví dụ hoạt động cơ bản:
try:
x = int(input("Enter a number: "))
except ValueError:
print("That wasn’t a number.")
else:
print("You entered", x)
finally:
print("Done.")
Khối except xử lý lỗi, else chỉ chạy nếu mọi thứ hoạt động, và finally luôn chạy, ngay cả khi bạn nhấn Ctrl‑C hoặc thoát.
Bạn có thể nắm vững các nền tảng xử lý đầu vào người dùng trong Python, từ nhắc cơ bản đến xác thực nâng cao và kỹ thuật xử lý lỗi qua bài Python User Input: Handling, Validation, and Best Practices.
Xử lý nhiều Exception
Đôi khi, một except là chưa đủ. Các nhánh mã khác nhau có thể thất bại theo những cách khác nhau, và bắt mọi thứ bừa bãi với một bare except: thì không hữu ích; nó có thể che giấu lỗi và khiến việc gỡ lỗi đau đầu.
Giả sử bạn đang cố chuyển đổi gì đó sang số nguyên rồi chia nó. Đây là điều không nên làm:
try:
result = int(user_input) / 2
except:
print("Something went wrong.")
Cách này bắt tất cả, bao gồm cả những thứ bạn có lẽ không định che đi, như lỗi gõ tên biến hoặc các exception bất ngờ chỉ ra vấn đề thực sự.
Thay vào đó, hãy cụ thể:
try:
result = int(user_input) / 2
except ValueError:
print("That wasn't a number.")
except ZeroDivisionError:
print("Division by zero?")
Nếu bạn có nhiều loại exception cần xử lý giống nhau, bạn có thể nhóm chúng như sau:
try:
result = some_function()
except (TypeError, ValueError):
print("Something was wrong with the data.")
Bằng cách này, bạn vẫn rõ ràng về những gì có thể xảy ra, mà không phải lặp lại cùng một khối nhiều lần.
Và còn có except Exception, tốt hơn một bare except, nhưng vẫn quá rộng. Tốt hơn là nhắm mục tiêu các lỗi cụ thể mà bạn dự đoán.
Sử dụng các khối else và finally
Khi mã trong khối try thành công và không phát sinh exception, Python sẽ thực thi khối else.
try:
user_id = get_user_id()
except LookupError:
print("User not found.")
else:
print("Welcome, user", user_id)
Điều này giúp bạn tách logic xử lý exception khỏi “đường đi hạnh phúc”, khiến mã dễ đọc hơn.
Tiếp theo là finally. Nó không quan tâm chuyện gì xảy ra; nó chạy dù thế nào. Có exception? Vẫn chạy. Không có exception? Vẫn chạy. Chương trình bị ngắt bằng Ctrl‑C? Vẫn chạy. Rất phù hợp cho việc dọn dẹp:
try:
f = open("data.txt")
process(f)
except IOError:
print("Couldn’t open file.")
finally:
f.close()
Chỉ cần nhớ, nếu tệp không mở được, f có thể chưa tồn tại, nên hãy cẩn thận. Bạn có thể dùng context manager của Python (with open(...) as f:) khi làm việc với tệp. An toàn hơn.
Nếu bạn muốn học các kỹ thuật then chốt, như xử lý exception và phòng tránh lỗi, để xử lý exception KeyError trong Python hiệu quả, tôi khuyên bạn xem bài Python KeyError Exceptions and How to Fix Them.
Thực tiễn tốt nhất và lỗi thường gặp với Python try-except
Thẳng thắn mà nói: viết các khối try‑except là một nghệ thuật. Làm tốt, chúng ngăn chương trình sập. Làm dở, chúng chôn lỗi sâu đến mức bạn không nhận ra cho đến khi ứng dụng sập và người dùng phàn nàn.
Dưới đây là một số thói quen đáng xây dựng:
Giữ khối try gọn. Đừng bọc cả trăm dòng mã trong một try. Như vậy chỉ khiến khó biết điều gì gây ra vấn đề. Thay vào đó, chỉ bọc phần mã có thể thất bại:
# Good
try:
value = int(data)
except ValueError:
print("Couldn’t convert.")
# Bad
try:
# Tons of unrelated logic
value = int(data)
do_more()
something_else()
except ValueError:
print("Huh?")
Tránh kiểm tra điều kiện trước khi hành động nếu dễ hơn là cứ thử và bắt lỗi. Python có tên cho điều này: EAFP, Easier to Ask Forgiveness than Permission. Nếu bạn kiểm tra tệp có tồn tại rồi mới mở, bạn đang tạo ra điều kiện tranh chấp. Thay vào đó:
try:
with open("file.txt") as f:
content = f.read()
except FileNotFoundError:
print("No file.")
Mẫu này tránh vấn đề phổ biến khi tệp có thể biến mất giữa lúc kiểm tra và lúc mở. Cứ thử làm. Nếu thất bại, bắt lỗi.
Cũng tránh việc “bịt miệng” lỗi bằng cách bắt tất cả và không làm gì. Đừng làm thế này:
try:
something()
except:
pass
Trừ khi bạn thực sự biết mình đang làm gì, đây là nơi lỗi ẩn nấp và sinh sôi. Nếu buộc phải bỏ qua, hãy cụ thể:
try:
something()
except TimeoutError:
# okay to ignore in this case
pass
Và hãy ghi log lỗi ở đâu đó. Nuốt chúng hoàn toàn đồng nghĩa với việc trong tương lai bạn sẽ không biết chuyện gì đã sai.
Exception dựng sẵn và Exception tùy chỉnh
Python có sẵn nhiều exception và chúng bao phủ đáng ngạc nhiên nhiều trường hợp. Có lẽ bạn đã gặp vài cái rồi:
ValueError: khi thứ gì đó có đúng kiểu nhưng giá trị không hợp lệ, nhưint("hello").TypeError: khi bạn cố dùng một phép toán trên kiểu không phù hợp, như cộng chuỗi và số.ZeroDivisionError: bạn đoán đúng rồi, chia cho 0.FileNotFoundError: cố mở một tệp không tồn tại.KeyError: khi một khóa thiếu trong dictionary.
Chúng hữu ích, và trong hầu hết script hay ứng dụng, là quá đủ. Nhưng đôi khi bạn sẽ muốn raise thứ gì đó mô tả hơn, thứ có ý nghĩa trong thế giới dự án của bạn, không chỉ của Python.
Giả sử bạn đang viết một ứng dụng xử lý đơn hàng trực tuyến, và bạn muốn raise lỗi khi người dùng cố mua thứ đã hết hàng. Bạn có thể dùng ValueError, nhưng hơi chung chung. Nó không nói cho người tiếp theo đọc mã của bạn biết chuyện gì đã xảy ra.
Đây là lúc exception tùy chỉnh phát huy tác dụng.
class OutOfStockError(Exception):
pass
def check_inventory(product_id):
if not in_stock(product_id):
raise OutOfStockError(f"Product {product_id} is out of stock.")
Bằng cách tạo exception tùy chỉnh, bạn đang bổ sung một lớp ý nghĩa. Bạn cũng có nhiều kiểm soát hơn; bạn có thể chỉ bắt đúng tình huống cụ thể này:
try:
check_inventory("shirt-001")
except OutOfStockError:
print("Sorry, that item is sold out.")
Đó là một điều nhỏ, nhưng giúp mã của bạn dễ hiểu và bảo trì hơn, đặc biệt trong các dự án lớn. Và nó phối hợp tốt với logging và giám sát, tên exception tùy chỉnh dễ tìm kiếm hơn nhiều so với một ValueError mơ hồ.
Logging, re-raise và xâu chuỗi exception
Có khoảnh khắc khi gỡ lỗi: bạn thấy một traceback, nhìn chằm chằm vào nó và nhận ra bạn không biết vì sao thứ gì đó thất bại. Đó là lúc logging xuất hiện. Không chỉ là ghi lại lỗi, mà là để cung cấp manh mối cho bạn (hoặc đội bạn) trong tương lai tìm ra chuyện gì đã sai.
Giả sử bạn đang bắt một exception và muốn ghi log trước khi tiếp tục:
import logging
logging.basicConfig(level=logging.ERROR)
try:
do_something()
except ValueError as e:
logging.error("Failed to do something: %s", e)
raise
Lệnh raise ở cuối rất quan trọng, nó ném lại cùng exception sau khi đã ghi log. Không có nó, bạn vừa nuốt lỗi. Đôi khi vậy cũng ổn, nhưng thường thì không.
Rồi đến mẹo raise from. Cái này dùng khi bạn đang xử lý một lỗi nhưng cần raise lỗi khác, và bạn không muốn mất lỗi gốc. Python cho phép xâu chuỗi chúng:
try:
connect_to_database()
except TimeoutError as e:
raise ConnectionError("Database unavailable.") from e
Theo cách này, traceback kể toàn bộ câu chuyện. Bạn nhận được ConnectionError mới, nhưng cũng thấy TimeoutError đã gây ra nó.
Bạn cũng có thể ẩn lỗi gốc (thường là không nên) như sau:
raise ConnectionError("Just this error, nothing else.") from None
Nhưng trừ khi bạn có lý do chính đáng, giữ đầy đủ chuỗi lỗi sẽ giúp mọi người hiểu chuyện gì đã sai và nó leo thang thế nào.
Bạn có thể tìm hiểu nền tảng về logging trong Python từ Hướng dẫn Logging trong Python.
Ví dụ và trường hợp sử dụng thực tế
Bàn về xử lý exception ở mức trừu tượng là một chuyện, nhưng nó “bật sáng” khi bạn thấy nó trong mã thật.
Lấy ví dụ đầu vào người dùng. Hỏi bất kỳ ai từng xây dựng công cụ dòng lệnh hay bộ xác thực biểu mẫu: người dùng sẽ nhập những thứ kỳ lạ nhất. Bạn yêu cầu một con số? Sẽ có người gõ “twelve”. Hoặc dán một số điện thoại. Hoặc chỉ nhấn Enter. Chuyện xảy ra thường xuyên.
Thay vì viết một danh sách dài các kiểm tra “nếu thì”, bạn có thể làm thế này:
while True:
user_input = input("Enter a number: ")
try:
number = int(user_input)
break
except ValueError:
print("Try again with a whole number.")
Đoạn mã này không hoảng khi nhận đầu vào rác. Nó bảo người dùng thử lại và lặp cho đến khi mọi thứ ổn. Gọn gàng hơn nhiều so với chồng chất các if cho mọi trường hợp rìa.
Một ví dụ khác: đọc từ tệp có thể không tồn tại.
try:
with open("config.json") as f:
settings = f.read()
except FileNotFoundError:
print("Missing config file. Using defaults.")
settings = "{}"
Không cần kiểm tra tệp có đó không. Cứ thử mở, và nếu thất bại, tiếp tục. Nếu bạn kiểm tra trước (os.path.exists()), có thể ai đó đã xóa tệp giữa lúc kiểm tra và lúc mở. Đó là điều kiện tranh chấp, không phải thứ bạn muốn gỡ.
Yêu cầu mạng là mỏ vàng cho exception. Bạn không thể luôn tin internet sẽ “ngoan”. Máy chủ sập. Kết nối rớt. DNS lỗi. Vì vậy nếu bạn làm điều như sau:
import requests
try:
response = requests.get("https://example.com/data")
response.raise_for_status()
except requests.exceptions.RequestException as e:
print("Network problem:", e)
Lớp cơ sở RequestException thuận tiện bắt hầu như mọi thứ requests có thể raise, từ timeout đến phản hồi lỗi. Bạn không phải viết mười khối except khác nhau trừ khi bạn muốn xử lý khác biệt.
Và nếu bạn viết script tự động hay dịch vụ backend, bọc logic chính trong các khối try‑except có thể là khác biệt giữa việc một tác vụ thất bại và cả hệ thống tối đen. Bạn muốn lỗi được ghi lại, tác vụ có thể phục hồi được thử lại, và tác vụ không thể phục hồi được dừng lại gọn gàng, không phải với những stack trace khó hiểu cuộn dài trong log.
Tìm hiểu về tự động hóa bằng Python, gồm các khái niệm nền tảng, thư viện chính, làm việc với dữ liệu, sử dụng cải tiến bởi AI và thực tiễn tốt nhất trong bài Python Automation: A Complete Guide.
Các cách dùng nâng cao của try-except trong Python
Đến giờ, có lẽ bạn đã thấy try‑except linh hoạt thế nào. Nhưng linh hoạt cũng có mặt trái. Rất dễ từ hữu ích thành cẩu thả mà không cố ý. Dưới đây là cách giữ mọi thứ trong tầm kiểm soát.
Bắt các exception cụ thể. Nếu bạn biết có thể xảy ra điều gì, hãy nêu tên nó:
try:
result = int(data)
except ValueError:
# Only catches invalid numbers, not everything else under the sun
Tránh bare except:. Đừng làm thế trừ khi bạn xử lý điều gì đó rất đặc biệt. Nó sẽ bắt cả KeyboardInterrupt, SystemExit, và những thứ khác bạn có lẽ không muốn “bịt miệng”.
Dùng else và finally khi chúng làm mã rõ ràng hơn. Đừng cố dùng chỉ vì chúng tồn tại. Nếu luồng bình thường của mã đang bị chôn trong try, có lẽ nên chuyển nó sang else.
Giữ các khối try nhỏ. Bạn càng cho vào nhiều, càng khó xác định dòng nào gây lỗi. Chỉ bọc phần có thể thất bại.
Ghi log lỗi khi cần, đặc biệt trong môi trường sản xuất. Dù bạn không sập chương trình, biết cái gì (và khi nào) thất bại sẽ khiến việc gỡ lỗi sau này dễ hơn nhiều.
Exception tùy chỉnh không bắt buộc, nhưng hữu ích. Nếu bạn có các vấn đề đặc thù ứng dụng, hãy định nghĩa lỗi của riêng bạn. Chúng có thể làm log dễ đọc hơn và mã tự giải thích hơn.
Một điều cuối: đừng dùng exception cho điều khiển luồng trừ khi không còn cách tốt hơn. Rất cám dỗ để viết logic kiểu “thử cái này; nếu thất bại thì làm cái kia,” nhưng nếu đó là chuyện bạn mong đợi xảy ra thường xuyên, có lẽ có cách sạch hơn.
Kết luận
Như tôi đã giải thích trong suốt bài viết, xử lý exception không chỉ là tránh sập. Đó là viết mã biết trước rắc rối, xử lý chúng bình tĩnh, và tiếp tục, hoặc dừng lại theo cách hợp lý. Đó là khác biệt giữa việc người dùng nhận được thông điệp hữu ích và bị trả về terminal với một traceback dài, khó đọc.
Tôi rất khuyến nghị học thêm về exception, kèm các bài tập thực hành, trong chương Catching Exceptions in Python của khóa học Lập trình hướng đối tượng (OOP) trong Python của chúng tôi.
Câu hỏi thường gặp về Python try-except
Sự khác biệt giữa error và exception trong Python là gì?
Error thường chỉ vấn đề ngăn Python thậm chí bắt đầu chạy mã của bạn, như thiếu dấu hai chấm hoặc gõ sai từ khóa. Đây là lỗi cú pháp. Exception xảy ra khi mã đang chạy, chẳng hạn cố chia cho 0 hoặc mở một tệp không tồn tại. Exception có thể được bắt và xử lý để chương trình của bạn không sập.
Dùng bare except trong Python có tệ không?
Có, trong hầu hết trường hợp. Một bare except bắt mọi thứ, bao gồm cả những thứ bạn không định bắt, như tín hiệu ngắt bàn phím hoặc tín hiệu thoát hệ thống. Điều đó khiến gỡ lỗi khó khăn. Tốt hơn là bắt các exception cụ thể, như ValueError hoặc FileNotFoundError, để bạn biết chính xác mình đang xử lý gì.
Khi nào tôi nên dùng else trong một khối try-except?
Hãy dùng else khi bạn muốn chạy một đoạn mã chỉ khi không có exception xảy ra trong khối try. Nó giúp tách logic đường đi thành công khỏi logic xử lý lỗi, khiến mã dễ đọc và bảo trì hơn.
Nếu tôi đã có một khối except thì finally để làm gì?
finally chạy trong mọi trường hợp, dù có lỗi hay không. Nó hoàn hảo cho dọn dẹp: đóng tệp, giải phóng tài nguyên, rollback giao dịch, v.v. Ngay cả khi có lỗi xảy ra hoặc bạn thoát sớm, finally vẫn chạy.
Tôi có nên luôn dùng `try-except` thay vì kiểm tra điều kiện trước không?
Không phải lúc nào cũng vậy, nhưng thường tốt hơn là cứ try làm và catch lỗi nếu thất bại. Các nhà phát triển Python gọi đây là EAFP, “Dễ xin tha thứ hơn là xin phép.” Cách này nhanh hơn và tránh được một số lỗi, đặc biệt khi thứ gì đó có thể thay đổi giữa lúc kiểm tra và lúc hành động (như tệp bị xóa).
