academy

Custom Exceptions in Ruby

Robert Beekman

Robert Beekman on

Custom Exceptions in Ruby

A little while ago we talked about exceptions in Ruby. This time we explore ways of creating custom exceptions specific to your app’s needs.

Let's say we have a method that handles the uploading of images while only allowing JPEG images that are between 100 Kilobytes and 10 Megabytes. To enforce these rules we raise an exception every time an image violates them.

1class ImageHandler
2  def self.handle_upload(image)
3    raise "Image is too big" if image.size > 10.megabytes
4    raise "Image is too small" if image.size < 100.kilobytes
5    raise "Image is not a JPEG" unless %w[JPG JPEG].include?(image.extension)
6
7    #… do stuff
8  end
9end

Every time a user uploads an image that doesn't meet the rules, our (Rails) web app displays the default Rails 502 error page for the uncaught error.

1class ImageUploadController < ApplicationController
2  def upload
3    @image = params[:image]
4    ImageHandler.handle_upload(@image)
5
6    redirect_to :index, :notice => "Image upload success!"
7  end
8end

The Rails generic error page doesn't offer the user much help, so let's see if we can improve on these errors. We have two goals: inform the user when the file size is outside the set bounds and prevent hackers from uploading potentially malicious (non-JPEG) files, by returning a 403 forbidden status code.

Custom error types

Almost everything in Ruby is an object, and errors are no exception. This means that we can subclass from any error class and create our own. We can use these custom error types in our handle_upload method for different validations.

1class ImageHandler
2  # Domain specific errors
3  class ImageExtensionError < StandardError; end
4  class ImageTooBigError < StandardError
5    def message
6      "Image is too big"
7    end
8  end
9  class ImageTooSmallError < StandardError
10    def message
11      "Image is too small"
12    end
13  end
14
15  def self.handle_upload(image)
16    raise ImageTooBigError if image.size > 10.megabytes
17    raise ImageTooSmallError if image.size < 100.kilobytes
18    raise ImageExtensionError unless %w[JPG JPEG].include?(image.extension)
19
20    #… do stuff
21  end
22end

First, we've added three new classes to the handler that extend from StandardError. For the image size errors, we've overridden the message method of StandardError with an error message we can show to users. The way raise was called in the handle_upload method has also changed, by replacing the custom StandardError message with a different error type we can raise a different, more specific, error.

Now, we can use these custom error types in our controller to return different responses to errors. For instance, we can return the specific error message or a specific response code.

1class ImageUploadController < ApplicationController
2  def upload
3    @image = params[:image]
4    ImageHandler.handle_upload(@image)
5
6    redirect_to :index, :notice => "Image upload success!"
7
8  rescue ImageHandler::ImageTooBigError, ImageHandler::ImageTooSmallError => e
9    render "edit", :alert => "Error: #{e.message}"
10
11  rescue ImageHandler::ImageExtensionError
12    head :forbidden
13  end
14end

This is already a lot better than using the standard raise calls. With a little bit more subclassing we can make it make it easier to use, by rescuing entire error groups rather than every error type separately.

1class ImageHandler
2  class ImageExtensionError < StandardError; end
3  class ImageDimensionError < StandardError; end
4  class ImageTooBigError < ImageDimensionError
5    def message
6      "Image is too big"
7    end
8  end
9  class ImageTooSmallError < ImageDimensionError
10    def message
11      "Image is too small"
12    end
13  end
14
15  def self.handle_upload(image)
16    raise ImageTooBigError if image.size > 10.megabytes
17    raise ImageTooSmallError if image.size < 100.kilobytes
18    raise ImageExtensionError unless %w(JPG JPEG).include?(image.extension)
19
20    #… do stuff
21  end
22end

Instead of rescuing every separate image dimension exception, we can now rescue the parent class ImageDimensionError. This will rescue both our ImageTooBigError and ImageTooSmallError.

1class ImageUploadController < ApplicationController
2  def upload
3    @image = params[:image]
4    ImageHandler.handle_upload(@image)
5
6    redirect_to :index, :notice => "Image upload success!"
7
8  rescue ImageHandler::ImageDimensionError => e
9    render "edit", :alert => "Error: #{e.message}"
10
11  rescue ImageHandler::ImageExtensionError
12    head :forbidden
13  end
14end

The most common case for using your own error classes is when you write a gem. The mongo-ruby-driver gem is a good example of the use of custom errors. Each operation that could result in an exception has its own exception class, making it easier to handle specific use cases and generate clear exception messages and classes.

Another advantage of using custom exception classes is that when using exception monitoring tools like AppSignal. These tools give you a better idea as to where exceptions occurred, as well as grouping similar errors in the user interface.

Have any questions about raising or catching exceptions in Ruby? Please don’t hesitate to let us know at @AppSignal. If you have any comments regarding the article or if you have any topics that you'd like us to cover, then please get in touch with us.

Share this article

RSS
Robert Beekman

Robert Beekman

As a co-founder, Robert wrote our very first commit. He's also our support role-model and knows all about the tiny details in code. Travels and photographs (at the same time).

All articles by Robert Beekman

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps