Custom Errors in Crystal

# A custom error type usually has the suffix "Error".
class ArgError < Exception
  getter arg : Int32
  getter message : String

  def initialize(@arg : Int32, @message : String)
  end

  # Adding this to_s method makes ArgError implement
  # the Exception interface.
  def to_s(io : IO) : Nil
    io << "#{@arg} - #{@message}"
  end
end

def f(arg : Int32) : {Int32, Exception?}
  if arg == 42
    # Return our custom error.
    return {-1, ArgError.new(arg, "can't work with it")}
  end
  {arg + 3, nil}
end

# Crystal doesn't have a direct equivalent to Go's errors.As,
# but we can achieve similar functionality using is_a?
_, err = f(42)
if err.is_a?(ArgError)
  puts err.arg
  puts err.message
else
  puts "err doesn't match ArgError"
end

This Crystal code demonstrates how to create and use custom error types. Here’s a breakdown of the translation:

  1. We define a custom ArgError class that inherits from Exception. This is equivalent to implementing the error interface in the original code.

  2. The ArgError class has arg and message properties, similar to the struct in the original code.

  3. We implement a to_s method, which is Crystal’s equivalent of the Error() method in the original code.

  4. The f function is translated almost directly, but we use Crystal’s tuple syntax to return multiple values.

  5. In the main function equivalent, we use Crystal’s is_a? method to check if the error is an instance of ArgError. This is similar to the errors.As functionality in the original code.

To run this program:

$ crystal run custom_errors.cr
42
can't work with it

Crystal’s error handling is somewhat different from Go’s. It uses exceptions rather than returning error values. However, this example demonstrates how to create a custom error type and use it in a way that’s similar to the Go code.

Crystal doesn’t have a direct equivalent to Go’s errors.As function, but the is_a? method provides similar functionality for type checking and casting.