Exception trong Rails: rescue đúng tầng, log đúng context

Exception handling không phải nghệ thuật làm cho error biến mất. Mục tiêu là giữ invariant, trả response phù hợp và để lại đủ context cho người trực production—thường là chính ta, nhưng ít ngủ hơn.

Rescue exception cụ thể

1
2
3
4
5
begin
user = User.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: "user_not_found" }, status: :not_found
end

Tránh:

1
rescue Exception

Exception bao gồm tín hiệu hệ thống như SystemExit, Interrupt, NoMemoryError. Thông thường chỉ rescue subclass của StandardError, tốt nhất là class cụ thể bạn thật sự xử lý được.

rescue_from ở controller boundary

1
2
3
4
5
6
7
8
9
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found

private

def render_not_found
render json: { error: "not_found" }, status: :not_found
end
end

Đây là boundary tốt để map domain/framework exception sang HTTP. Không biến mọi exception thành 200 OK kèm { success: false }; status code tồn tại vì client cũng có cảm xúc.

Service object và transaction

1
2
3
4
5
6
7
8
9
10
11
12
13
class TransferMoney
class InsufficientBalance < StandardError; end

def call(from:, to:, amount:)
ApplicationRecord.transaction do
from.lock!
raise InsufficientBalance if from.balance < amount

from.update!(balance: from.balance - amount)
to.update!(balance: to.balance + amount)
end
end
end

Raise khi invariant bị vi phạm; transaction rollback. Chỉ rescue ở nơi có quyết định meaningful: retry, translate, compensate hoặc trả response.

Log có context, không log secret

1
2
3
4
5
6
Rails.logger.error(
event: "transfer_failed",
transfer_id: transfer.id,
error_class: error.class.name,
error_message: error.message
)

APM/error tracker nên giữ stack trace và request correlation ID. Không log password, token, full payment data hoặc nguyên params.

Background jobs

Retry chỉ phù hợp với lỗi tạm thời. Validation failure hay record không tồn tại thường không tự lành sau 17 lần retry.

Job có side effect nên idempotent hoặc có deduplication. Nếu lần đầu đã charge tiền nhưng response mất, retry mù là cách biến reliability thành chương trình khách hàng thân thiết.

ensure

Dùng ensure để cleanup resource:

1
2
3
4
5
6
file = File.open(path)
begin
process(file)
ensure
file.close
end

Ưu tiên block API khi có thể vì Ruby tự cleanup:

1
File.open(path) { |file| process(file) }

Checklist

  • Rescue class cụ thể.
  • Không nuốt error rồi trả dữ liệu giả.
  • Giữ transaction/invariant.
  • Retry có giới hạn và chỉ cho lỗi transient.
  • Log context, không log secret.
  • Map exception sang HTTP ở boundary.
  • Để error không xử lý được tiếp tục raise tới monitoring.

References