+1

Cơ bản về xử lý ngoại lệ trong Ruby

Một ngoại lệ là gì?

Ngoại lệ là cách mà Ruby xử lý với những sự kiện không mong muốn xảy ra. Nếu bạn đã từng gặp phải những lỗi về đánh máy(typo) trong code của mình, làm crash chương trình với những lỗi như SyntaxError hay NoMethodError và đó là một hành động ngoại lệ. Khi bạn đưa ra một ngoại lệ trong Ruby, toàn bộ chương trình của bạn dừng lại và bắt đầu tắt. Nếu không có gì ngăn chặn mọi tiến trình thì chương trình của bạn sẽ thoát ra với một thông báo lỗi. Một ví dụ. Ở đoạn code dưới đây, chúng ta thử chia một số cho 0. Điều này là không thể, do đó Ruby sẽ đưa ra một ngoại lệ gọi là ZeroDivisionError. Chương trình dừng lại và in ra một thông báo lỗi.

1 / 0
# Program crashes and outputs: "ZeroDivisionError: divided by 0"

Khi chương trình bị crash sẽ khiến cho người dùng cảm thấy bực bội. Do vậy thông thường chúng ta không muốn dừng tiến trình lại và xử lý lỗi một cách thông minh. Nó được gọi là rescuing, handling hay catching ngoại lệ(bắt ngoại lệ). Những thuật ngữ đó là tương đương nhau. Đây là cách mà bạn làm trong Ruby:

begin
  # Any exceptions in here... 
  1/0
rescue
  # ...will cause this code to run
  puts "Got an exception, but I'm responding intelligently!"
  do_something_intelligent()
end

# This program does not crash.
# Outputs: "Got an exception, but I'm responding intelligently!"

Ngoại lệ vẫn sảy ra, nhưng nó sẽ không làm cho chương trình sụp đổ bởi vì nó đã được giải cứu(rescued). Thay vì thoát chương trình, Ruby thực thi đoạn code trong block rescue, nó in ra thông báo lỗi. Điều này thật tốt, nhưng nó có một hạn chế lớn. Nó nói với chúng ta rằng "Đã có lỗi sảy ra" nhưng lại không cho chúng ta biết lỗi gì đã sảy ra. Tất cả thông tin về lỗi gì đã sảy ra sẽ được chứa trong một đối tượng ngoại lệ.

Đối tượng ngoại lệ (Exception Objects)

Đối tượng ngoại là những đối tượng cơ bản của Ruby. Nó chứa tất cả dữ liệu về những gì đã sảy ra cho một ngoại lệ mà bạn đã giải cứu. Để lấy đối tượng ngoại lệ, bạn có thể sử dụng cú pháp như sau:

# Rescues all errors, an puts the exception object in `e`
rescue => e

# Rescues only ZeroDivisionError and puts the exception object in `e`
rescue ZeroDivisionError => e

Ở ví dụ thứ 2 bên trên, ZeroDivisionError là một class của đối tượng trong e Những đối tượng ngoại lệ cũng giữ những thông tin hữu ích giúp cho việc debug.

begin
  # Any exceptions in here... 
  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...

Đưa ra ngoại lệ riêng của bạn

Đến đây chúng ta đã nói về xử lý ngoại lệ. Bạn có thể cũng đưa ra ngoại lệ của mình. Việc này được gọi là raising, bạn thực hiện nó bằng cách gọi phương thức raise. Khi bạn đưa ra một ngoại lệ, bạn có thể chọn loại ngoại lệ để sử dụng. Bạn cũng có thể thiết lập thông báo lỗi. Dưới đây là ví dụ:

begin
  # raises an ArgumentError with the 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 tạo ra một đối tượng error(một ArgumentError) với một thông báo tùy chỉnh ("You messed up!") và truyền nó vào phương thức raise. Bạn có thể sử dụng một trong các cách sau để gọi phương thức raise:

# This is my favorite because it's so explicit
raise RuntimeError.new("You messed up!")

# ...produces the same result
raise RuntimeError, "You messed up!"

# ...produces the same result. But you can only raise 
# RuntimeErrors this way
raise "You messed up!"

Tạo một ngoại lệ tùy chỉnh (Custom Exceptions)

Ruby cung cập một số lượng lớn các class ngoại lệ, nhưng không thể cho tất cả các trường hợp có thể xảy ra. Để tạo một ngoại lệ tùy chỉnh, chỉ cần tạo một class nó kế thừa từ StandarError.

class PermissionDeniedError < StandardError

end

raise PermissionDeniedError.new()

Nó cũng như những class Ruby thông thường. Điều đó có nghĩa là bạn có thể thêm phương thức và dữ liệu cho nó. Hãy thêm một thuộc tính action:

class PermissionDeniedError < StandardError

  attr_reader :action

  def initialize(message, action)
    # Call the parent's constructor to set the message
    super(message)

    # Store the action in an instance variable
    @action = action
  end

end

# Then, when the user tries to delete something they don't
# have permission to delete, you might do something like this:
raise PermissionDeniedError.new("Permission Denied", :delete)

Những class Exception

Chúng ta vừa tạo một ngoại lệ mà là class con của StandarError, nó cũng là class con của Exception. Mọi class ngoại lệ trong Ruby đề có cha gốc là Exception.

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 cần phải nhơ tất cả chúng. Nhưng cần phải hiểu được ý tưởng của việc thiết kế kế thừa các class ngoại lệ bởi lý do quan trọng sau:

Bắt ngoại lệ ở một class cụ thể cũng đồng thời bắt ngoại lệ ở những class con của nó. Khi bạn bắt ngoại lệ StandarError, bạn không chỉ bắt ngoại lẹ với riêng lớp đó mà còn ở class con của nó là ArgumentError, IOError, ... Đó là một ý tưởng tồi

Bắt ngoại lệ không xác định, một ý tưởng tồi (Rescuing All Exceptions (the bad way))

Nếu bạn muốn các advisor của mình mắng hãy viết đoạn code sau =))

// Don't do this 
begin
  do_something()
rescue Exception => e
  ...
end

Đoạn mã trên xẽ bắt mọi ngoại lệ. Đừng làm thế, nó sẽ làm chương trình của bạn trở nên kỳ quoặc Đó là vì Ruby sử dụng ngoại lệ cho những thứ khác ngoài lỗi. Nó cũng sử dụng chúng để xử lý tin nhắn từ hệ điều hành được gọi là "Tín hiệu." Nếu bạn đã từng nhấn "ctrl-c" để thoát khỏi chương trình, bạn đã sử dụng một tín hiệu. Bằng cách loại bỏ tất cả các ngoại lệ, bạn cũng loại bỏ các tín hiệu đó.

Bắt tất cả lỗi, cách làm đúng (Rescuing All Errors (The Right Way))

Bạn muốn bắt tất cả ngoại lệ là con của StandarError

begin
  do_something()
rescue StandardError => e
  # Only your app's exceptions are swallowed. Things like SyntaxErrror are left alone. 
end

Nếu bạn không xác định một lỗi cụ thể, Ruby giả định bạn sử dụng StandarError

begin
  do_something()
rescue => e
  # This is the same as rescuing StandardError
end

Bắt một lỗi cụ thể, cách tiếp cận tốt nhất (Rescuing Specific Errors (The Best Approach))

Bạn đã biết cách để bắt tất cả các lỗi, nhưng khi bạn làm điều đó nó chỉ thể hiện rằng bạn quá lười để xác định một lỗi cụ thể nào đó, và nó sẽ quay lại ám ảnh bạn. Do vậy việc làm đúng đắn là xác định chính xác ngoại lệ muốn bắt

begin
  do_something()
rescue Errno::ETIMEDOUT => e
  // This will only rescue Errno::ETIMEDOUT exceptions
end

Hoặc có thể xử lý nhiều ngoại lệ

begin
  do_something()
rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED => e
end

Kết

Việc xử lý ngoại lệ có ở hầu hết các ngôn ngữ bậc cao, việc hiểu và áp dụng đúng là rất quan trọng Bài viết được dịch từ bài A Beginner's Guide to Exceptions in Ruby, trong blog còn rất nhiều bài hay về bắt ngoại lệ trong Ruby từ cơ bản đến nâng cao các bạn có thể tham khảo. Cảm ơn các bạn đã theo dõi!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí