+3

Ruby Exceptions

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 raisefail 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

Viblo
Let's register a Viblo Account to get more interesting posts.