5 Best Practices for Handling Controllers in Ruby on Rails
Catch the Correct Errors
Bad practice: Catching all exceptions indiscriminately.
# app/controllers/posts_controller.rb
def create
@post.create(post_params)
rescue => e
logger.error "Unhandled exception: #{e}"
end
This approach will suppress all errors, making debugging difficult and hiding issues that might require immediate attention.
Good practice: Catch specific exceptions relevant to the context.
# app/controllers/payments_controller.rb
def process_payment
PaymentGateway.charge!(current_user, amount)
rescue PaymentGateway::PaymentFailed => e
logger.error "Payment failed: #{e}"
redirect_to :back, alert: "Payment failed: #{e.message}"
end
Specifying the exception type makes the error handling focused and reduces the suppression of unexpected errors.
You can also define a constant variable with all the errors:
# /config/intializers/custom/errors.rb
STRIPE_ERRORS = [
# All the errors
]
# app/controllers/payments_controller.rb
def process_payment
PaymentGateway.charge!(current_user, amount)
rescue *STRIPE_ERRORS => e
logger.error "Payment failed: #{e}"
redirect_to :back, alert: "Payment failed: #{e.message}"
end
This really helps in terms of developer experience to understand what to expect.
Truly RESTful APIs
Bad practice: Unclear or inexistant status management.
# app/controllers/api/v1/books_controller.rb
def update
if @book.update(book_params)
render json: @book
else
render json: @book.errors
end
end
Good practice: Use Rails’ symbolic status codes for clarity.
# app/controllers/api/v1/books_controller.rb
def update
if @book.update(book_params)
render json: @book, status: :ok
else
render json: @book.errors, status: :unprocessable_entity
end
end
You are supposed to understand what to do with the response solely by using the status code.
It’s bad practice if you need to parse the body to understand what to do next.
Proper Timeout Settings
Bad practice: Using default or overly long timeouts.
# config/initializers/timeout.rb
Rack::Timeout.timeout = 120 # seconds
Long timeouts can lead to slow application responses under high load or services failures.
Good practice: Set reasonable timeouts based on the expected response times of external services.
# config/initializers/timeout.rb
Rack::Timeout.timeout = 10 # seconds
Shorter, more appropriate timeouts help maintain application performance and better user experiences.
Code to Background
Bad practice: Handling long-running tasks synchronously.
# app/controllers/orders_controller.rb
def create
@order.process_payment
@order.ship_order
render json: @order
end
Processing payments and shipping orders in the request cycle can lead to timeouts and poor user experiences.
Good practice: Use background jobs for heavy lifting.
# app/controllers/orders_controller.rb
def create
OrderJob.perform_later(@order)
render json: @order, status: :accepted
end
perform_later
method enqueues the job to be performed asynchronously, improving response times and scalability.
Leverage Gems
Bad practice: Writing custom code for functionality that is already well-handled by existing libraries.
# lib/custom_parser.rb
class CustomParser
def self.parse(html)
# complex parsing logic
end
end
Maintaining custom parsing logic can be error-prone and costly in terms of development time.
Good practice: Use a gem like Nokogiri for HTML and XML parsing.
# Gemfile
gem 'nokogiri'
# app/services/parser_service.rb
require 'nokogiri'
class ParserService
def parse(html)
Nokogiri::HTML(html)
end
end
This last point is a bit trivial. In the Ruby on Rails community, we don’t reinvent the wheel and we love our gems, unlike the JS community.
The point that I’m trying to make here is that you should be able to “gemify” sections of your logic to make it simpler to read and maintain.
We’ll keep that for next time!
Related posts
Building a Rails Engine