Exception Handling

In Crystal, the keyword raise is used to raise exceptions. When raised, the exception will unwind the call stack and it'll hopefully be caught by the programmer somewhere higher up in the method chain. If it's not caught, the program will exit with an unhandled exception error.

If you bubble up an exception with raise and catch it, the regular execution of the program will continue.

Handling exceptions with begin - rescue blocks:

arr = [] of Int8
puts "Enter an Int8 value:"
while number = gets
  number = number.strip
  if number == "" || number == "stop"
    break
  end

  # Catching exceptions in the begin block:
  begin
    arr << number.to_i8
  rescue ex
    # Reacting to the exception:
    p ex.message
    puts "The integer you've entered is bigger than Int8 capacity"
    exit
  else
    # In case where everything is good:
    p "Int8 added to the array."
    p arr
  ensure
    # Ensure block executes no matter what:
    p "Clean up with ensure block..."
  end
end

It's also possible to use a shorter syntax alternative:

def add_i8(arr, number)
  arr << number.to_i8
rescue
  p ex.message
  puts "The integer you've entered is bigger than Int8 capacity"
end

Shorter variant with the addition of ensure:

def add_i8(arr, number)
  arr << number.to_i8
rescue
  p ex.message
  puts "The integer you've entered is bigger than Int8 capacity"
ensure
  p "clean up happened."
end

A note on performance:

Using predicates like to_i? or to_f? with if blocks is much faster and less resource intensive compared to exception handling because exception handling allocates more memory. If you choose to use begin-rescue blocks, try to keep the amount of code between begin and rescue minimal.

Idiomatic Patterns

An idiomatic pattern is to return nil from an inner function, and catch an outer function which raises an exception when it detects the nil return. This exception then can be caught by the caller site.

Here's an example:

# Private implementation
def say_hi(name : String) : String | Nil
  name.size > 2 ? "Hi, #{name}" : nil
end

# Public implementation
def say_hi_public(name : String)
  msg = say_hi name
  unless msg
    raise "say_hi error: Name too short"
  else
    msg
  end
end

# Caller site
begin
  p say_hi_public "Jo"
rescue error
  p error
end

Let's rewrite the previous example using shorter syntax where possible:

# Private implementation
def say_hi(name : String) : String?
  name.size > 2 ? "Hi, #{name}" : nil
end

# Public implementation:

def say_hi_public(name : String)
  raise "say_hi error: Name too short" unless say_hi name
end

# Caller site:
def caller_site
  say_hi_public("Jo") # <Exception:say_hi error: Name too short>
rescue ex
  p ex
end

caller_site