Exception handling

Crystal’s way to do error handling is by raising and rescuing exceptions.

Raising exception

You raise exceptions by invoking a top-level raise method. Unlike other keywords, raise is a regular method with two overloads: one accepting a String and another accepting an Exception instance:

  1. raise "OH NO!"
  2. raise Exception.new("Some error")

The String version just creates a new Exception instance with that message.

Only Exception instances or subclasses can be raised.

Defining custom exceptions

To define a custom exception type, just subclass from Exception:

  1. class MyException < Exception
  2. end
  3. class MyOtherException < Exception
  4. end

You can, as always, define a constructor for your exception or just use the default one.

Rescuing exceptions

To rescue any exception use a begin ... rescue ... end expression:

  1. begin
  2. raise "OH NO!"
  3. rescue
  4. puts "Rescued!"
  5. end
  6. # Output: Rescued!

To access the rescued exception you can specify a variable in the rescue clause:

  1. begin
  2. raise "OH NO!"
  3. rescue ex
  4. puts ex.message
  5. end
  6. # Output: OH NO!

To rescue just one type of exception (or any of its subclasses):

  1. begin
  2. raise MyException.new("OH NO!")
  3. rescue MyException
  4. puts "Rescued MyException"
  5. end
  6. # Output: Rescued MyException

And to access it, use a syntax similar to type restrictions:

  1. begin
  2. raise MyException.new("OH NO!")
  3. rescue ex : MyException
  4. puts "Rescued MyException: #{ex.message}"
  5. end
  6. # Output: Rescued MyException: OH NO!

Multiple rescue clauses can be specified:

  1. begin
  2. # ...
  3. rescue ex1 : MyException
  4. # only MyException...
  5. rescue ex2 : MyOtherException
  6. # only MyOtherException...
  7. rescue
  8. # any other kind of exception
  9. end

You can also rescue multiple exception types at once by specifying a union type:

  1. begin
  2. # ...
  3. rescue ex : MyException | MyOtherException
  4. # only MyException or MyOtherException
  5. rescue
  6. # any other kind of exception
  7. end

else

An else clause is executed only if no exceptions were rescued:

  1. begin
  2. something_dangerous
  3. rescue
  4. # execute this if an exception is raised
  5. else
  6. # execute this if an exception isn't raised
  7. end

An else clause can only be specified if at least one rescue clause is specified.

ensure

An ensure clause is executed at the end of a begin ... end or begin ... rescue ... end expression regardless of whether an exception was raised or not:

  1. begin
  2. something_dangerous
  3. ensure
  4. puts "Cleanup..."
  5. end
  6. # Will print "Cleanup..." after invoking something_dangerous,
  7. # regardless of whether it raised or not

Or:

  1. begin
  2. something_dangerous
  3. rescue
  4. # ...
  5. else
  6. # ...
  7. ensure
  8. # this will always be executed
  9. end

ensure clauses are usually used for clean up, freeing resources, etc.

Short syntax form

Exception handling has a short syntax form: assume a method or block definition is an implicit begin ... end expression, then specify rescue, else, and ensure clauses:

  1. def some_method
  2. something_dangerous
  3. rescue
  4. # execute if an exception is raised
  5. end
  6. # The above is the same as:
  7. def some_method
  8. begin
  9. something_dangerous
  10. rescue
  11. # execute if an exception is raised
  12. end
  13. end

With ensure:

  1. def some_method
  2. something_dangerous
  3. ensure
  4. # always execute this
  5. end
  6. # The above is the same as:
  7. def some_method
  8. begin
  9. something_dangerous
  10. ensure
  11. # always execute this
  12. end
  13. end
  14. # Similarly, the shorthand also works with blocks:
  15. (1..10).each do |n|
  16. # potentially dangerous operation
  17. rescue
  18. #..
  19. else
  20. #..
  21. ensure
  22. #..
  23. end

Type inference

Variables declared inside the begin part of an exception handler also get the Nil type when considered inside a rescue or ensure body. For example:

  1. begin
  2. a = something_dangerous_that_returns_Int32
  3. ensure
  4. puts a + 1 # error, undefined method '+' for Nil
  5. end

The above happens even if something_dangerous_that_returns_Int32 never raises, or if a was assigned a value and then a method that potentially raises is executed:

  1. begin
  2. a = 1
  3. something_dangerous
  4. ensure
  5. puts a + 1 # error, undefined method '+' for Nil
  6. end

Although it is obvious that a will always be assigned a value, the compiler will still think a might never had a chance to be initialized. Even though this logic might improve in the future, right now it forces you to keep your exception handlers to their necessary minimum, making the code’s intention more clear:

  1. # Clearer than the above: `a` doesn't need
  2. # to be in the exception handling code.
  3. a = 1
  4. begin
  5. something_dangerous
  6. ensure
  7. puts a + 1 # works
  8. end

Alternative ways to do error handling

Although exceptions are available as one of the mechanisms for handling errors, they are not your only choice. Raising an exception involves allocating memory, and executing an exception handler is generally slow.

The standard library usually provides a couple of methods to accomplish something: one raises, one returns nil. For example:

  1. array = [1, 2, 3]
  2. array[4] # raises because of IndexError
  3. array[4]? # returns nil because of index out of bounds

The usual convention is to provide an alternative “question” method to signal that this variant of the method returns nil instead of raising. This lets the user choose whether she wants to deal with exceptions or with nil. Note, however, that this is not available for every method out there, as exceptions are still the preferred way because they don’t pollute the code with error handling logic.