Ruby Exceptions
Bài đăng này đã không được cập nhật trong 7 năm
Bài viết này sẽ thảo luận về các ngoại lệ trong ruby (ruby exceptions). Thoạt nhìn thì chúng ta thấy đây có vẻ là một khái niệm đơn giản. Tuy nhiên sẽ có một vài điểm cần lưu ý.
Exception trong ruby
Nếu bạn không biết về ruby exception thì có thể thử ví dụ sau đây:
puts a # => undefined local variable or method `a' for main:Object (NameError)
Ở ví dụ trên ta thấy a là 1 biến chưa được định nghĩa và vì vậy sẽ có lỗi trả về "undefined local variable or method a" và kiểu của nó là "NameError"
Nếu muốn đoạn code trên chạy, chúng ta sử dụng begin...rescue
để bắt ngoại lệ
begin
puts a
rescue
puts 'Something bad happened'
end
Chúng ta có thể bắt các ngoại lệ khác nhau bới rescue
bằng cách sau:
begin
puts a
rescue NameError => e
puts e.message
end
# or
def foo
begin
# logic
rescue NoMemoryError, StandardError => e
# process the error
end
end
Bảng kế thừa của ngoại lệ trong Ruby
Một điều quan trọng cần biết về ruby exception là mặc định rescue
sẽ bắt tất cả các lỗi kế thừa từ StandardError
. Vì vậy nó sẽ không bắt NotImplementedError
hay NoMemoryError
ở ví dụ sử dụng rescue thứ nhất. Để biết được những exception nào chúng ta có thể bắt được bởi rescue chúng ta hay xem bảng kế thừa sau:
- Exception
- NoMemoryError
- ScriptError
- LoadError
- NotImplementedError
- SyntaxError
- SecurityError
- SignalException
- Interrupt
- StandardError -- default for rescue
- ArgumentError
- UncaughtThrowError
- EncodingError
- FiberError
- IOError
- EOFError
- IndexError
- KeyError
- StopIteration
- LocalJumpError
- NameError
- NoMethodError
- RangeError
- FloatDomainError
- RegexpError
- RuntimeError -- default for raise
- SystemCallError
- Errno:
- ThreadError
- TypeError
- ZeroDivisionError
- SystemExit
- SystemStackError
Truy cập đối tượng Exception
Chúng ta còn có thể định nghĩa biến để truy cập vào đối tượng Exception như sau:
def foo
begin
raise 'here'
rescue => e
e.backtrace # ["test.rb:3:in `foo'", "test.rb:10:in `<main>'"]
e.message # 'here'
end
end
Các bạn có thể tìm hiểu thêm về các phương thức của đối tượng Exception ở đây.
Ensure
Ruby cung cấp cho chúng ta một từ khóa rất thú vị để làm việc với exception đó là ensure
. Bất kể việc exception có xảy ra hay không, Ruby chắc chắn sẽ thực hiện đoạn code bên trong ensure
. Và thông thường từ khóa này hay được sử dụng để đóng kết nối database hay xóa các tệp tạm thời, ...
begin
puts a
rescue NameError => e
puts e.message
ensure
# clean up the system, close db connection, remove tmp file, etc
end
Có một điều quan trọng mà chúng ta cần biết về ensure
, đó là nếu chúng ta sử dụng return
trong ensure
mà không định nghĩa rescue
thì ensure
sẽ chặn ngoại lệ. Chúng ta sẽ rõ ràng hơn qua ví dụ sau:
# Không return trong ensure
def foo
begin
raise 'here'
ensure
puts 'processed'
end
end
foo
# processed
# => `foo': here (RuntimeError)
# Có return trong ensure
def foo
begin
raise 'here'
ensure
return 'processed'
end
end
puts foo # => processed
Không có ngoại lệ RuntimeError
ở lần này! Ruby đã trả về "processed" từ ensure
và không đưa ra ngoại lệ!
Raise
Bây giờ chúng ta sẽ học cách làm thế nào để đẩy ra ngoại lệ từ code. Module Kernel
có method raise
cho phép đẩy ra ngoại lệ. Có một alias method giống với raise
là fail
nhưng thông thường chúng ta sẽ thấy raise được sử dụng nhiều hơn.
Nếu chúng ta gọi raise mà không có params thì nó sẽ có lỗi như sau:
raise # => `<main>': unhandled exception
Trường hợp này sẽ không có thông báo tên lỗi cho dev nên thường chúng ta sẽ thêm params thông báo lỗi cho raise:
raise 'Could not read from database' # => Could not read from database (RuntimeError)
Bây giờ chúng ta sẽ thấy raise
trả về thông báo lỗi và kiểu exception là RuntimeError
bởi mặc định raise sẽ đẩy ra exception là RuntimeError
.
Chúng ta cũng có thể định nghĩa chính xác exception mình muốn đẩy ra như sau:
raise NotImplementedError, 'Method not implemented yet'
# => Method not implemented yet (NotImplementedError)
Một điều thú vị là raise
sẽ gọi đến method #exception cho bất kỳ class nào khi bạn truy cập vào nó. Trong trường hợp này nó gọi đến NotImplementedError#exception. Điều này cho phép chúng ta thêm exception support cho bất kỳ class nào. Yêu cầu chính là cần phải có method #exception:
class Response
def exception(message = 'HTTP Error')
RuntimeError.new(message)
end
end
response = Response.new
raise response # => HTTP Error (RuntimeError)
Một điều thú vị nữa về exception là khi exception xảy ra Ruby lưu nó trong biến global là $!
$! # => nil
begin
raise 'Exception'
rescue
$! # <RuntimeError: Exception>
end
$! # => nil
Chúng ta có thể thấy là ngoại lệ được lưu ở biến $!
chỉ khi nó xảy ra còn ở bên ngoài nó vẫn có giá trị là nil.
Retry
Ruby cung cấp cho chúng ta cách để chạy đoạn code bên trong begin
thêm 1 lần nữa. Hãy tưởng tượng rằng chúng ta có 1 service mà một vài trường hợp nó lại không trả về data yêu cầu. Giải pháp là chúng ta có thể gói service vào vòng lặp, tuy nhiên có cách khác là chúng ta sử dụng keyword retry
, nó sẽ thực hiện đoạn code bên trong begin thêm 1 lần nữa.
tries = 0
begin
tries += 1
puts "Trying #{tries}..."
raise 'Did not work'
rescue
retry if tries < 3
puts 'I give up'
end
# Trying 1...
# Trying 2...
# Trying 3...
# I give up
Đoạn code trên khá đơn giản. Nếu code bên trong begin đẩy ra 1 exception thì chúng ta sẽ thực hiện lại nó 1 lấn nữa. Ý tưởng này rất thú vị, nhưng tôi thấy rằng để xử lý lỗi trong service thứ 3 tốt hơn thì có giải pháp gọi là Circuit Braker. Các bạn có thể tham khảo giải pháp đó ở đây: article, gem.
Một vài điều thú vị khác về exception
Từ khóa at_exit
:
at_exit { puts 'going to exit' }
raise 'exception'
# => going to exit
# => exception (RuntimeError)
Đoạn code bên trong khối at_exit
sẽ được chạy trước khi đẩy ra exception.
Một điều thú vị cuối cùng giúp chúng ta viết gọn đoạn code hơn khi sử dụng từ khóa rescue
:
def foo
# ...
rescue
# ...
end
# sẽ tốt hơn là
def foo
begin
# ...
rescue
# ...
end
end
Kết luận
Bài viết được dịch từ nguồn: http://rubyblog.pro/2017/03/exceptions Mong rằng bài viết này sẽ giúp ích được cho các bạn.
All rights reserved