Ruby standard error

Never rescue Exception in Ruby! - Maybe you've heard this before. It's good advice, but it's pretty confusing unless you're already in the know. Let's break this statement down and see what it means.

«Never rescue Exception in Ruby!»

Maybe you’ve heard this before. It’s good advice, but it’s pretty confusing unless you’re already in the know. Let’s break this statement down and see what it means.

You probably know that in Ruby, you can rescue exceptions like so:

begin
  do_something()
rescue => e
  puts e # e is an exception object containing info about the error. 
end

And you can rescue specific errors by providing the classname of the error.

begin
  do_something()
rescue ActiveRecord::RecordNotFound => e
  puts e # Only rescues RecordNotFound exceptions, or classes that inherit from RecordNotFound
end

Every type of exception in Ruby is just a class. In the example above, ActiveRecord::RecordNotFound is just the name of a class that follows certain conventions.

This is important because when you rescue RecordNotFound, you also rescue any exceptions that inherit from it.

Why you shouldn’t rescue Exception

The problem with rescuing Exception is that it actually rescues every exception that inherits from Exception. Which is….all of them!

That’s a problem because there are some exceptions that are used internally by Ruby. They don’t have anything to do with your app, and swallowing them will cause bad things to happen.

Here are a few of the big ones:

  • SignalException::Interrupt — If you rescue this, you can’t exit your app by hitting control-c.

  • ScriptError::SyntaxError — Swallowing syntax errors means that things like puts("Forgot something) will fail silently.

  • NoMemoryError — Wanna know what happens when your program keeps running after it uses up all the RAM? Me neither.

begin
  do_something()
rescue Exception => e
  # Don't do this. This will swallow every single exception. Nothing gets past it. 
end

I’m guessing that you don’t really want to swallow any of these system-level exceptions. You only want to catch all of your application level errors. The exceptions caused YOUR code.

Luckily, there’s an easy way to to this.

Rescue StandardError Instead

All of the exceptions that you should care about inherit from StandardError. These are our old friends:

  • NoMethodError — raised when you try to invoke a method that doesn’t exist

  • TypeError — caused by things like 1 + ""

  • RuntimeError — who could forget good old RuntimeError?

To rescue errors like these, you’ll want to rescue StandardError. You COULD do it by writing something like this:

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

But Ruby has made it much easier for use.

When you don’t specify an exception class at all, ruby assumes you mean StandardError. So the code below is identical to the above code:

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

Custom Exceptions Should Inherit from StandardError

So what does this mean for you if you’re creating your own custom exceptions?

It means you should always inherit from StandardError, and NEVER from Exception. Inheriting from Exception is bad because it breaks the expected behavior of rescue. People will think they’re rescuing all application-level errors but yours will just sail on through.

class SomethingBad < StandardError
end

raise SomethingBad

The Exception Tree

Since Ruby’s exceptions are implemented in a class heirarchy, it can be helpful to see it laid out. Below is a list of exception classes that ship with Ruby’s standard library. Third-party gems like rails will add additional exception classes to this chart, but they will all inherit from some class on this list.

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
 fatal

Built-in Exception Classes

NoMemoryError

Raised when memory allocation fails.

NoMemoryError Reference

ScriptError

ScriptError is the superclass for errors raised when a script can not be executed because of a LoadError, NotImplementedError or a SyntaxError. Note these type of ScriptErrors are not StandardError and will not be rescued unless it is specified explicitly (or its ancestor Exception).

ScriptError Reference

LoadError

Raised when a file required (a Ruby script, extension library, …) fails to load.

require 'this/file/does/not/exist'

raises the exception:

LoadError: no such file to load -- this/file/does/not/exist

LoadError Reference

NotImplementedError

Raised when a feature is not implemented on the current platform. For example, methods depending on the fsync or fork system calls may raise this exception if the underlying operating system or Ruby runtime does not support them.

Note that if fork raises a NotImplementedError, then respond_to?(:fork) returns false.

NotImplementedError Reference

SyntaxError

Raised when encountering Ruby code with an invalid syntax.

raises the exception:

SyntaxError: (eval):1: syntax error, unexpected '=', expecting $end

SyntaxError Reference

SecurityError

No longer used by internal code.

SecurityError Reference

SignalException

Raised when a signal is received.

begin
  Process.kill('HUP',Process.pid)
  sleep # wait for receiver to handle signal sent by Process.kill
rescue SignalException => e
  puts "received Exception #{e}"
end

produces:

received Exception SIGHUP

SignalException Reference

Interrupt

Raised when the interrupt signal is received, typically because the user has pressed Control-C (on most posix platforms). As such, it is a subclass of SignalException.

begin
  puts "Press ctrl-C when you get bored"
  loop {}
rescue Interrupt => e
  puts "Note: You will typically use Signal.trap instead."
end

produces:

Press ctrl-C when you get bored

then waits until it is interrupted with Control-C and then prints:

Note: You will typically use Signal.trap instead.

Interrupt Reference

StandardError

The most standard error types are subclasses of StandardError. A rescue clause without an explicit Exception class will rescue all StandardErrors (and only those).

def foo
  raise "Oups"
end
foo rescue "Hello"   #=> "Hello"

On the other hand:

require 'does/not/exist' rescue "Hi"

raises the exception:

LoadError: no such file to load -- does/not/exist

StandardError Reference

ArgumentError

Raised when the arguments are wrong and there isn’t a more specific Exception class.

Ex: passing the wrong number of arguments

raises the exception:

ArgumentError: wrong number of arguments (given 2, expected 1)

Ex: passing an argument that is not acceptable:

raises the exception:

ArgumentError: negative array size

ArgumentError Reference

UncaughtThrowError

Raised when throw is called with a tag which does not have corresponding catch block.

raises the exception:

UncaughtThrowError: uncaught throw "foo"

UncaughtThrowError Reference

EncodingError

EncodingError is the base class for encoding errors.

EncodingError Reference

FiberError

Raised when an invalid operation is attempted on a Fiber, in particular when attempting to call/resume a dead fiber, attempting to yield from the root fiber, or calling a fiber across threads.

fiber = Fiber.new{}
fiber.resume #=> nil
fiber.resume #=> FiberError: dead fiber called

FiberError Reference

IOError

Raised when an IO operation fails.

File.open("/etc/hosts") {|f| f << "example"}
  #=> IOError: not opened for writing

File.open("/etc/hosts") {|f| f.close; f.read }
  #=> IOError: closed stream

Note that some IO failures raise `SystemCallError’s and these are not subclasses of IOError:

File.open("does/not/exist")
  #=> Errno::ENOENT: No such file or directory - does/not/exist

IOError Reference

EOFError

Raised by some IO operations when reaching the end of file. Many IO methods exist in two forms,

one that returns nil when the end of file is reached, the other raises EOFError.

EOFError is a subclass of IOError.

file = File.open("/etc/hosts")
file.read
file.gets     #=> nil
file.readline #=> EOFError: end of file reached

EOFError Reference

IndexError

Raised when the given index is invalid.

a = [:foo, :bar]
a.fetch(0)   #=> :foo
a[4]         #=> nil
a.fetch(4)   #=> IndexError: index 4 outside of array bounds: -2...2

IndexError Reference

KeyError

Raised when the specified key is not found. It is a subclass of IndexError.

h = {"foo" => :bar}
h.fetch("foo") #=> :bar
h.fetch("baz") #=> KeyError: key not found: "baz"

KeyError Reference

StopIteration

Raised to stop the iteration, in particular by Enumerator#next. It is rescued by Kernel#loop.

loop do
  puts "Hello"
  raise StopIteration
  puts "World"
end
puts "Done!"

produces:

StopIteration Reference

ClosedQueueError

The exception class which will be raised when pushing into a closed Queue. See Queue#close and SizedQueue#close.

ClosedQueueError Reference

LocalJumpError

Raised when Ruby can’t yield as requested.

A typical scenario is attempting to yield when no block is given:

def call_block
  yield 42
end
call_block

raises the exception:

LocalJumpError: no block given (yield)

A more subtle example:

def get_me_a_return
  Proc.new { return 42 }
end
get_me_a_return.call

raises the exception:

LocalJumpError: unexpected return

LocalJumpError Reference

NameError

Raised when a given name is invalid or undefined.

raises the exception:

NameError: undefined local variable or method `foo` for main:Object

Since constant names must start with a capital:

Integer.const_set :answer, 42

raises the exception:

NameError: wrong constant name answer

NameError Reference

NoMethodError

Raised when a method is called on a receiver which doesn’t have it defined and also fails to respond with method_missing.

raises the exception:

NoMethodError: undefined method `to_ary` for "hello":String

NoMethodError Reference

RangeError

Raised when a given numerical value is out of range.

raises the exception:

RangeError: bignum too big to convert into `long`

RangeError Reference

FloatDomainError

Raised when attempting to convert special float values (in particular Infinity or NaN) to numerical classes which don’t support them.

Float::INFINITY.to_r   #=> FloatDomainError: Infinity

FloatDomainError Reference

RegexpError

Raised when given an invalid regexp expression.

raises the exception:

RegexpError: target of repeat operator is not specified: /?/

RegexpError Reference

RuntimeError

A generic error class raised when an invalid operation is attempted. Kernel#raise will raise a RuntimeError if no Exception class is specified.

raises the exception:

RuntimeError Reference

FrozenError

Raised when there is an attempt to modify a frozen object.

raises the exception:

FrozenError: can't modify frozen Array

FrozenError Reference

SystemCallError

SystemCallError is the base class for all low-level platform-dependent errors.

The errors available on the current platform are subclasses of SystemCallError and are defined in the Errno module.

File.open("does/not/exist")

raises the exception:

Errno::ENOENT: No such file or directory - does/not/exist

SystemCallError Reference

Errno

Ruby exception objects are subclasses of Exception. However, operating systems typically report errors using plain integers. Module Errno is created dynamically to map these operating system errors to Ruby classes, with each error number generating its own subclass of SystemCallError. As the subclass is created in module Errno, its name will start Errno::.

The names of the Errno:: classes depend on the environment in which Ruby runs. On a typical Unix or Windows platform, there are Errno classes such as Errno::EACCES, Errno::EAGAIN, Errno::EINTR, and so on.

The integer operating system error number corresponding to a particular error is available as the class constant Errno::error::Errno.

Errno::EACCES::Errno   #=> 13
Errno::EAGAIN::Errno   #=> 11
Errno::EINTR::Errno    #=> 4

The full list of operating system errors on your particular platform are available as the constants of Errno.

Errno.constants   #=> :E2BIG, :EACCES, :EADDRINUSE, :EADDRNOTAVAIL, ...

Errno Reference

ThreadError

Raised when an invalid operation is attempted on a thread.

For example, when no other thread has been started:

This will raises the following exception:

ThreadError: stopping only thread
note: use sleep to stop forever

ThreadError Reference

TypeError

Raised when encountering an object that is not of the expected type.

raises the exception:

TypeError: no implicit conversion of String into Integer

TypeError Reference

ZeroDivisionError

Raised when attempting to divide an integer by 0.

42 / 0   #=> ZeroDivisionError: divided by 0

Note that only division by an exact 0 will raise the exception:

42 /  0.0   #=> Float::INFINITY
42 / -0.0   #=> -Float::INFINITY
0  /  0.0   #=> NaN

ZeroDivisionError Reference

SystemExit

Raised by exit to initiate the termination of the script.

SystemExit Reference

SystemStackError

Raised in case of a stack overflow.

def me_myself_and_i
  me_myself_and_i
end
me_myself_and_i

raises the exception:

SystemStackError: stack level too deep

SystemStackError Reference

fatal

fatal is an Exception that is raised when Ruby has encountered a fatal error and must exit.

fatal Reference

Handling exceptions in your API applications is quite an important thing, and if you want to keep things DRY, you should think how to do it in the proper way. In our Ruby on Rails API course, I’ve shown how to implement the error handling using ErrorSerializer and ActiveModelSerializers gem and here I’m going to show you even better approach to this topic when you can unify EVERY error across the whole API application.

Rails api thumbnail

Ruby On Rails REST API
The complete guide

Create professional API applications that you can hook anything into! Learn how to code like professionals using Test Driven Development!

Take this course!

UPDATE: I’ve recently came with even greater way of handling Errors in Rails Web applications using «dry-monads»! It still uses this approah to serilize the errors for JSON:API purposes, but the actual mapping can be done in the more neat way!

The final approach

There is no point to cover the whole thought process of how we came with the final result, but if you’re interested in any particular part just say it in the comments. The basic assumptions were to keep things DRY and unified across the whole application.

So here is the code.

The standard error.
    # app/lib/errors/standard_error.rb

    module Errors
      class StandardError < ::StandardError
        def initialize(title: nil, detail: nil, status: nil, source: {})
          @title = title || "Something went wrong"
          @detail = detail || "We encountered unexpected error, but our developers had been already notified about it"
          @status = status || 500
          @source = source.deep_stringify_keys
        end

        def to_h
          {
            status: status,
            title: title,
            detail: detail,
            source: source
          }
        end

        def serializable_hash
          to_h
        end

        def to_s
          to_h.to_s
        end

        attr_reader :title, :detail, :status, :source
      end
    end

First of all we needed to have the Base error, which will be a fallback for any exception risen by our application. As we use JSON API in every server’s response, we wanted to always return an error in the format that JSON API describes.

We extracted all error-specific parts for every HTML status code we wanted to support, having a fallback to 500.

More detailed errors

As you can see this basic error was just a scaffold we could use to override particular attributes of the error object. Having that implemented, we were able to instantiate several case-specific errors to deliver more descriptive messages to our clients.

    # app/lib/errors/unauthorized.rb

    module Errors
      class Unauthorized < Errors::StandardError
        def initialize
          super(
            title: "Unauthorized",
            status: 401,
            detail: message || "You need to login to authorize this request.",
            source: { pointer: "/request/headers/authorization" }
          )
        end
      end
    end



    # app/lib/errors/not_found.rb

    module Errors
      class NotFound < Errors::StandardError
        def initialize
          super(
            title: "Record not Found",
            status: 404,
            detail: "We could not find the object you were looking for.",
            source: { pointer: "/request/url/:id" }
          )
        end
      end
    end

All errors are very clean and small, without any unnecessary logic involved. That’s reasonable as we don’t want to give them an opportunity to fail in an unexpected way, right?

Anyway defining the error objects is only the half of the job.

Serializing the error in Ruby Application

This approach above allowed us to use something like:

    ...
    def show
      Article.find(params[:id])
    rescue ActiveRecord::RecordNotFound
      e = Errors::NotFound.new
      render json: ErrorSerializer.new(e), status: e.status
    end
    ...

To serialize the standard responses we use fast_jsonapi gem from Netflix. It’s quite nice for the usual approach, but for error handling not so much so we decided to write our own ErrorSerializer.

    # app/serializers/error_serializer.rb

    class ErrorSerializer
      def initialize(error)
        @error = error
      end

      def to_h
        serializable_hash
      end

      def to_json(payload)
        to_h.to_json
      end

      private

      def serializable_hash
        {
          errors: Array.wrap(error.serializable_hash).flatten
        }
      end

      attr_reader :error
    end

The logic is simple. It accepts an object with status, title, detail and source methods, and creates the serialized responses in the format of:

    # json response

    {
      "errors": [
        {
          "status": 401,
          "title": "Unauthorized",
          "detail": "You need to login to authorize this request.",
          "source": {
            "pointer": "/request/headers/authorization"
          }
        }
      ]
    }

The only problem here is that handling all of those errors in every action of the system will end up with a lot of code duplications which is not very DRY, is it? I could just raise proper errors in the services, but standard errors, like ActiveRecord::RecordNotFound would be tricky. This is then what we ended up within our API ApplicationController:

    # app/controllers/application_controller.rb

    class ApplicationController < ActionController::API
      ...
      include Api::ErrorHandler
      ...
    end

We just included the ErrorHandler module, where we implemented all mappings and the logic responsible for all error handling.

    module Api::ErrorHandler
      extend ActiveSupport::Concern

      ERRORS = {
        'ActiveRecord::RecordNotFound' => 'Errors::NotFound',
        'Driggl::Authenticator::AuthorizationError' => 'Errors::Unauthorized',
        'Pundit::NotAuthorizedError' => 'Errors::Forbidden'
      }

      included do
        rescue_from(StandardError, with: lambda { |e| handle_error(e) })
      end

      private

      def handle_error(e)
        mapped = map_error(e)
        # notify about unexpected_error unless mapped
        mapped ||= Errors::StandardError.new
        render_error(mapped)
      end

      def map_error(e)
        error_klass = e.class.name
        return e if ERRORS.values.include?(error_klass)
        ERRORS[error_klass]&.constantize&.new
      end

      def render_error(error)
        render json: Api::V1::ErrorSerializer.new([error]), status: error.status
      end
    end

At the top, we added a nice mapper for all errors we expect to happen somewhere. Then we rescue from the default error for the rescue block, which is the StandardError, and call the handle_error method with the risen object.

Inside of this method we just do the mapping of the risen error to what we have server responses prepared to. If none of them matches, we fall back to our Errors::StandardError object so client always gets the nice error message in the server response.

We can also add extra notifiers for any error that is not mapped in the handler module, so application admins will be able to track the unexpected results.

Rising errors in the application

In Driggl we managed to create a unified solution for the whole error handling across our API application. This way we can raise our errors in a clean way without repeating any rescue blocks, and our ApplicationController will always handle that properly.

    def show
      Article.find!(params[:id])
    end

or

    def authorize!
      raise Errors::Unauthorized unless currentuser
    end

Handling validation errors

Well, that is a nice solution, but there is one thing we intentionally omitted so far and it is: validation failure.

The problem with validations is that we can’t write the error object for invalid request just as we did for the rest, because:

  • the failure message differs based on the object type and based on attributes that are invalid
  • one JSON response can have multiple errors in the returned array.

This requires us add one more error, named Invalid, which is an extended version of what we had before.

    # app/lib/errors/invalid.rb

    module Errors
      class Invalid < Errors::StandardError
        def initialize(errors: {})
          @errors = errors
          @status = 422
          @title = "Unprocessable Entity"
        end

        def serializable_hash
          errors.reduce([]) do |r, (att, msg)|
            r << {
              status: status,
              title: title,
              detail: msg,
              source: { pointer: "/data/attributes/#{att}" }
            }
          end
        end

        private

        attr_reader :errors
      end
    end

You can see that the main difference here is the serialized_hash and initialize method. The initialize method allows us to pass error messages hash into our error object, so then we can properly serialize the error for every single attribute and corresponding message.

Our ErrorSerializer should handle that out of the box, returning:

    # json response

    {
      "errors": [
        {
          "status": 422,
          "title": "Unprocessable entity",
          "detail": "Can't be blank",
          "source": {
            "pointer": "/data/attributes/title"
          }
        },
        {
          "status": 422,
          "title": "Unprocessable entity",
          "detail": "Can't be blank",
          "source": {
            "pointer": "/data/attributes/content"
          }
        }
      ]
    }

The last thing, however, is to rise it somewhere, so the handler will get the exact error data to proceed.

In the architecture we have, it’s a not big deal. It would be annoying if we would go with updating and creating objects like this:

    app/controllers/articles_controller.rb

    class ArticlesController < ApplicationController
      def create
        article = Article.new(article_params)
        article.save!
      end

      private

      def article_attributes
        params.permit(:title)
      end
    end 

As this would force us to rescue the ActiveRecord::RecordInvalid error in every action, and instantiate our custom error object there like this:

      def create
        article = Article.new(article_params)
        article.save!
      rescue ActiveRecord::RecordInvalid
        raise Errors::Invalid.new(article.errors.to_h)
      end

Which again would end up with repeating a lot of rescue blocks across the application.

In Driggl however, we do take advantage of Trailblazer architecture, with contracts and operations, which allows us to easily unify every controller action in the system.

    # app/controllers/articles_controller.rb

    class ArticlesController < ApplicationController
     def create
        process_operation!(Admin::Article::Operation::Create)
      end

      def update
        process_operation!(Admin::Article::Operation::Update)
      end
    end

I won’t go into details of Trailbalzer in this article, but the point is that we could handle the validation errors once inside of the process_operation! method definition and everything works like a charm across the whole app, keeping things still nice and DRY

    # app/controllers/application_controller.rb

    class ApplicationController < ActionController::API
      private

      def process_operation!(klass)
        result = klass.(serialized_params)
        return render_success if result.success?
        raise Errors::Invalid.new(result['contract.default'].errors.to_h)
      end

      def serialized_params
        data = params[:data].merge(id: params[:id])
        data.reverse_merge(id: data[:id])
      end

      def render_success
        render json: serializer.new(result['model']), status: success_http_status
      end

      def success_http_status
        return 201 if params[:action] == 'create'
        return 204 if params[:action] == 'destroy'
        return 200
      end
    end

Summary

You could think it’s a lot of code, but really, for big applications it’s just nothing comparing to repeating it in hundred of controllers and other files. In this form we managed to unify all our errors across the whole API application and we don’t need to worry anymore about unexpected failures delivering to the client.

I hope this will be useful for you too, and if you’ll find any improvements for this approach, don’t hesitate to let me know in the comments!

Special Thanks:
  • Cristopher Jeschke for a nice cover image

Other resources: 

  • Custom exceptions in Ruby by Appsignal
  • Error Hierarchy in Ruby by Starr Horne

Понравилась статья? Поделить с друзьями:
  • Ruby runtime error
  • Ruby http error
  • Ruby error message
  • Ruby argument error
  • Ruantiblock update error another instance of update is already running