Practical1
Practical1

Reputation: 678

Why is this exception not being caught by the rescue block?

I ran this code:

begin
  print 'Enter something:'
  x = gets.to_i          # Enter a string
rescue => err
  print(err.to_s)
end

I don't get why the rescue block does not catch the exception. It always returns zero when a string is input, and doesn't trigger the rescue block. I don't know why it isn't working. Can anyone please help?

Upvotes: 2

Views: 910

Answers (1)

Todd A. Jacobs
Todd A. Jacobs

Reputation: 84453

Behavior Differs Between String#to_i and Kernel#Integer

The reason your exception handler is never called is because String#to_i doesn't raise an exception, even if it can't detect a valid integer within the String object. In such cases, it simply returns 0.

In comparison, the behavior of Kernel#Integer is more complex, but is expected to raise ArgumentError or TypeError if the contents of the string do not strictly conform to a numeric representation.

So, to minimally refactor your existing code to raise an exception on non-numeric inputs:

begin
  print 'Enter something: ' 
  x = Integer gets
rescue => err
  # Do something other than just print err on STDERR, which is the 
  # default behavior anyway. Perhaps send it to STDOUT instead.
  puts "I received an exception: #{err}"

  # After handling, re-raise the original exception with or without
  # passing the original exception object. `raise` and `raise err` 
  # will do the same thing here.
  raise

  # For more advanced uses, you can also do something else like raise 
  # a different exception (e.g. TypeError), or modify the exception
  # object stored in err and raise that modified object instead.
end

The following user inputs will each convert cleanly:

  • 1
  • 2
  • 0xff

The code will even handle initial/trailing spaces, newlines, and carriage returns in most cases, without any additional effort on your part. However:

  • Enter something: one
    ArgumentError: invalid value for Integer(): "one\n"

  • Enter something: "1"
    ArgumentError: invalid value for Integer(): "\"1\"\n"

  • Enter something: nil
    ArgumentError: invalid value for Integer(): "nil\n"

In general, you can rely on Kernel#Integer to raise an exception when necessary, which simplifies your code a lot. However, see caveats below.

Caveats

These examples don't require it, but you might also want to sanitize your input with #strip, #chomp, or other string transformations when necessary. Your mileage in this regard will vary greatly with your real-world use case, but while Kernel#Integer generally does the right thing, and Ruby encourages relying on exceptions to handle non-standard edge cases, it's often unwise to trust user-tainted inputs.

It's also worth noting that both String#to_i and Kernel#Integer might operate on values other than user input, in which case know that Integer(nil) will raise:

Integer nil
TypeError: can't convert nil into Integer

This might be important. Again, your mileage may vary.

Upvotes: 5

Related Questions