Hôm kia tôi đang tìm một giới thiệu về Ruby exceptions viết cho người mới - những người biết cú pháp Ruby cơ bản nhưng không thực sự chắc exception là gì hoặc tại sao nó hữu ích. Tôi không tìm thấy, nên tôi quyết định thử làm nó mình. Hy vọng bạn thấy nó hữu ích. Nếu có điểm nào gây nhầm lẫn, thoải mái tweet cho tôi @StarrHorne. :)
Exception là gì?
Exceptions là cách của Ruby để deal với các sự kiện bất ngờ.
Nếu bạn từng làm typo trong mã, khiến chương trình crash với thông báo như SyntaxError hoặc NoMethodError, thì bạn đã thấy exceptions hoạt động.
Khi bạn raise một exception trong Ruby, thế giới dừng lại và chương trình của bạn bắt đầu shutdown. Nếu không có gì dừng quá trình, chương trình của bạn sẽ cuối cùng thoát với thông báo lỗi.
Đây là ví dụ. Trong mã dưới đây, chúng ta thử chia cho 0. Điều này không thể, nên Ruby raise một exception tên ZeroDivisionError. Chương trình quit và in thông báo lỗi.
1 / 0
# Program crashes and outputs: "ZeroDivisionError: divided by 0"
Chương trình crash tend to làm user giận. Vì vậy chúng ta thường muốn dừng quá trình shutdown này, và phản ứng với lỗi một cách thông minh.
Điều này gọi là "rescuing," "handling," hoặc "catching" một exception. Chúng đều có cùng nghĩa. Đây là cách làm trong Ruby:
begin
# Bất kỳ exception nào ở đây...
1/0
rescue
# ...sẽ khiến mã này chạy
puts "Got an exception, but I'm responding intelligently!"
do_something_intelligent()
end
# Chương trình này không crash.
# Outputs: "Got an exception, but I'm responding intelligently!"
Exception vẫn xảy ra, nhưng nó không khiến chương trình crash vì nó đã được "rescued." Thay vì thoát, Ruby chạy mã trong rescue block, in ra thông báo.
Điều này hay, nhưng nó có một hạn chế lớn. Nó cho chúng ta biết "có gì đó sai," mà không cho chúng ta biết cái gì sai.
Tất cả thông tin về cái gì sai sẽ được chứa trong một exception object.
Exception Objects
Exception objects là object Ruby bình thường. Chúng giữ tất cả dữ liệu về "cái gì xảy ra" cho exception bạn vừa rescue.
Để lấy exception object, bạn sẽ dùng cú pháp rescue hơi khác.
# Rescues all errors, và puts exception object trong `e`
rescue => e
# Rescues chỉ ZeroDivisionError và puts exception object trong `e`
rescue ZeroDivisionError => e
Trong ví dụ thứ hai trên, ZeroDivisionError là class của object trong e. Tất cả các "loại" exception chúng ta đã nói về thực sự chỉ là tên class.
Exception objects cũng giữ dữ liệu debug hữu ích. Hãy xem exception object cho ZeroDivisionError của chúng ta.
begin
# Bất kỳ exception nào ở đây...
1/0
rescue ZeroDivisionError => e
puts "Exception Class: #{ e.class.name }"
puts "Exception Message: #{ e.message }"
puts "Exception Backtrace: #{ e.backtrace }"
end
# Outputs:
# Exception Class: ZeroDivisionError
# Exception Message: divided by 0
# Exception Backtrace: ...backtrace as an array...
Giống như hầu hết Ruby exceptions, nó chứa message và backtrace cùng với tên class.
Raise Your Own Exceptions
Cho đến nay chúng ta chỉ nói về rescue exceptions. Bạn cũng có thể trigger exception của riêng bạn. Quá trình này gọi là "raising." Bạn làm nó bằng cách gọi phương thức raise.
Khi bạn raise exception của mình, bạn được chọn loại exception nào dùng. Bạn cũng được set error message.
Đây là ví dụ:
begin
# raises một ArgumentError với message "you messed up!"
raise ArgumentError.new("You messed up!")
rescue ArgumentError => e
puts e.message
end
# Outputs: You messed up!
Như bạn thấy, chúng ta đang tạo object lỗi mới (một ArgumentError) với message tùy chỉnh ("You messed up!") và pass nó cho phương thức raise.
Đây là Ruby, raise có thể gọi theo vài cách:
# Cách này tôi thích nhất vì nó rõ ràng
raise RuntimeError.new("You messed up!")
# ...tạo ra kết quả giống nhau
raise RuntimeError, "You messed up!"
# ...tạo ra kết quả giống nhau. Nhưng bạn chỉ có thể raise
# RuntimeErrors theo cách này
raise "You messed up!"
Tạo Custom Exceptions
Built-in exceptions của Ruby tuyệt vời, nhưng chúng không cover mọi use case có thể.
Điều gì nếu bạn đang xây dựng hệ thống user và muốn raise exception khi user cố truy cập phần off-limits của site? Không có standard exceptions của Ruby fit, nên cách tốt nhất là tạo một loại exception mới.
Để tạo custom exception, chỉ cần tạo class mới kế thừa từ StandardError.
class PermissionDeniedError < StandardError
end
raise PermissionDeniedError.new()
Đây chỉ là class Ruby bình thường. Nghĩa là bạn có thể thêm method và data vào nó như bất kỳ class nào. Hãy thêm attribute tên "action":
class PermissionDeniedError < StandardError
attr_reader :action
def initialize(message, action)
# Gọi constructor của parent để set message
super(message)
# Lưu action trong instance variable
@action = action
end
end
# Sau đó, khi user thử xóa thứ họ không có
# permission để xóa, bạn có thể làm như sau:
raise PermissionDeniedError.new("Permission Denied", :delete)
Class Hierarchy
Chúng ta vừa tạo custom exception bằng cách subclass StandardError, mà chính nó subclass Exception.
Thực tế, nếu bạn nhìn class hierarchy của bất kỳ exception nào trong Ruby, bạn sẽ thấy nó cuối cùng dẫn về Exception. Ở đây, tôi sẽ chứng minh cho bạn. Đây là hầu hết built-in exceptions của Ruby, hiển thị theo hierarchy:
Exception
NoMemoryError
ScriptError
LoadError
NotImplementedError
SyntaxError
SignalException
Interrupt
StandardError
ArgumentError
IOError
EOFError
IndexError
LocalJumpError
NameError
NoMethodError
RangeError
FloatDomainError
RegexpError
RuntimeError
SecurityError
SystemCallError
SystemStackError
ThreadError
TypeError
ZeroDivisionError
SystemExit
Bạn không phải nhớ tất cả này, tất nhiên. Tôi chỉ show chúng cho bạn vì ý tưởng hierarchy này rất quan trọng cho một lý do cụ thể.
Rescue errors của một class cụ thể cũng rescue errors của child classes của nó.
Hãy thử lại...
Khi bạn rescue StandardError, bạn không chỉ rescue exceptions với class StandardError mà còn của children của nó. Nếu bạn nhìn chart, bạn sẽ thấy đó là nhiều: ArgumentError, IOError, v.v.
Nếu bạn rescue Exception, bạn sẽ rescue mọi exception đơn lẻ, đó sẽ là ý tưởng rất tệ.
Rescue All Exceptions (cách xấu)
Nếu bạn muốn bị quát, đi stack overflow và post mã trông như sau:
// Don't do this
begin
do_something()
rescue Exception => e
...
end
Mã trên sẽ rescue mọi exception. Đừng làm thế! Nó sẽ làm hỏng chương trình của bạn theo cách kỳ lạ.
Đó là vì Ruby dùng exceptions cho thứ khác ngoài errors. Nó cũng dùng chúng để handle messages từ operating system gọi là "Signals." Nếu bạn từng bấm "ctrl-c" để thoát chương trình, bạn đã dùng signal. Bằng cách suppress tất cả exceptions, bạn cũng suppress những signals đó.
Cũng có một số loại exceptions — như syntax errors — thực sự nên khiến chương trình crash. Nếu bạn suppress chúng, bạn sẽ không bao giờ biết khi bạn làm typo hoặc lỗi khác.
Rescue All Errors (Cách đúng)
Quay lại nhìn chart class hierarchy và bạn sẽ thấy tất cả errors bạn muốn rescue là children của StandardError
Nghĩa là nếu bạn muốn rescue "tất cả errors" bạn nên rescue StandardError.
begin
do_something()
rescue StandardError => e
# Chỉ exception của app bạn bị nuốt. Cái như SyntaxErrror được để yên.
end
Thực tế, nếu bạn không chỉ định exception class, Ruby giả định bạn muốn StandardError
begin
do_something()
rescue => e
# Đây giống như rescue StandardError
end
Rescue Specific Errors (Cách tốt nhất)
Bây giờ bạn biết cách rescue all errors, bạn nên biết rằng đó thường là ý tưởng tồi, code smell, considered harmful, v.v.
Chúng thường là dấu hiệu bạn quá lười để tìm ra exception cụ thể nào cần rescue. Và chúng gần như luôn quay lại ám bạn.
Vì vậy hãy dành thời gian và làm đúng. Rescue specific exceptions.
begin
do_something()
rescue Errno::ETIMEDOUT => e
// Chỉ rescue Errno::ETIMEDOUT exceptions
end
Bạn thậm chí có thể rescue nhiều loại exception trong cùng rescue block, nên không có cớ. :)
begin
do_something()
rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED => e
end