Skip to content

Exceptions and Throw

datanorris edited this page Jul 9, 2016 · 9 revisions

This page describes the Exception class and some of its subclasses, how exception objects are manipulated in Ruby, and the throw/catch feature (which does not employ Exception objects at all).

For details on how exceptions can be caught, see Ruby Expressions.

The Exception class

Exception is the root class of all exceptions in Ruby - only instances of Exception and its subclasses can be raised as exceptions. Exception objects store the following state:

  • A message, which describes the exception (e.g. "invalid value for x: y"), accessed by the message, to_s and == methods and modified by the initialize constructor method
  • A backtrace, accessed by the backtrace method and modified by the set_backtrace method

This Exception object state (and, in general, additional state stored by Exception subclasses defined by core/extension libraries) is stored in the object as special hidden instance variables - they can't be directly accessed in Ruby code, however they do get copied in cloning/duping operations.

Ruby generally does not access the state directly, rather it uses the accessor/mutator methods - you are not required to store the state using the default native facilities, and may override these methods to access and store state how and if you wish as long as you observe the method contracts. However, note how by default Exception#exception directly modifies internal state as described below.

Message

The message method is expected to return a String instance, or an object that can be converted to one via to_str. It is used by Ruby (along with class name, the backtrace method, and perhaps other VM information) when printing out details of an unhandled exception that caused program termination.

Backtrace

The backtrace method is expected to return one of the following:

  • An array of Strings, or String-convertible objects via to_str, that each describe a level of the Ruby stack in the backtrace from the innermost level outwards
  • A single String or String-convertible object, which Ruby will treat as being wrapped in an Array.
  • nil, which means no backtrace is associated with this object

The set_backtrace method is expected to store a backtrace object which can be subsequently accessed by the backtrace method. When the Ruby VM invokes set_backtrace it passes a valid backtrace object (i.e. an array of Strings).

Ruby code can build its own backtrace via Thread#backtrace (note totally unrelated to Exception#backtrace).

Ruby exception context

When Ruby enters a rescue or ensure clause, the exception that caused the clause to be entered is stored in a hidden dynamic variable - the exception context. No exception context is defined for ensure clauses if the clause was entered through normal execution, due to a control transfer statement, or due to a thread kill instruction.

Ruby code can access the exception context (via core-defined global variables and raise methods), however it is scoped differently to regular dynamic variables and other types of Ruby scoping. An exception context is scoped to cover code executing within the instance of the rescue/ensure clause associated with the exception, and recursively any methods/blocks that are called from within that clause, until another rescue/ensure clause is nested within which defines its own exception context. This is a purely runtime, stack-based scoping - the lexical location of a particular line of code or the closures it has access to have no bearing on what (if any) exception context is in scope for that line.

Raising exceptions

Ruby implements an unusual pattern for raising exceptions. Methods for raising exceptions, such as Kernel#raise, don't directly accept an Exception object to be thrown - they accept a template object from which an exception will be provided, by calling the template's exception method. This feature is basically here to permit some syntactic conveniences as shown below.

Raise methods

Ruby methods for raising exceptions (such as Kernel#raise, Kernel#fail, Thread#raise) are composed of 2 phases:

  • Create the exception
  • Raise the exception

In the case of Thread#raise, the exception is created, but isn't raised directly - it will be raised by the targeted thread.

All of these methods may be invoked with arguments as follows:

raise
raise <string>
raise <template>, [ <argument> ], [ <backtrace> ]

The exception creation phase works as follows:

  • With no arguments:
  • If an exception context exists, an exception will be provided by invoking .exception() on the exception context exception, with no arguments
  • Otherwise, an exception will be created via RuntimeError.new - note that this creation is actually deferred and will be performed at the beginning of the exception raising stage
  • With a <string>, which is either an instance of String or an object that can be converted to a String via to_str, an exception will be created via RuntimeError.new(<string>)
  • With a <template>, an exception will be provided by invoking <template>.exception() or, if <argument> is provided, <template>.exception(<argument>). If <backtrace> is provided, it will subsequently be stored on the exception via .set_backtrace(<backtrace>)

The exception raising phase works as follows:

  • Invokes .backtrace() on the exception to determine if it doesn't have a backtrace (i.e. returns nil)
  • If so, creates a backtrace and invokes .set_backtrace(<backtrace>) on the exception
  • Raises the exception

Exception templates

Any object can be an exception template (with one exception - nil), all it has to do is implement the exception method. exception is expected to return an instance of Exception (which may be the template object itself), and it's expected to take 1 optional argument, of any type, which is passed in directly from a Kernel#raise (or similar method) argument and is intended to serve as a template-interpreted parameter in the exception object creation process.

There are 2 default implementations of exception:

  • Exception::exception, which implements the default behaviour of Class#new
  • Exception.exception, which expects an optional single argument of self or a message, and either:
  • Returns itself, if called with no arguments, or with 1 argument which is itself - note that it is probably sensible to preserve this behaviour if you override exception
  • Creates a clone of itself (with the default implementation of Object#clone), and invokes the default implementation of Exception#initialize on the new object with exception's arguments
    • Note that initialize accepts 1 optional argument and will reset the internal backtrace state to nil, and the internal message state to the argument (or nil if not provided)

Examples

The default implementations of exception suggest, for example, the following constructs:

raise exc                       # Where exc is an Exception object, raises it directly
raise exc, exc, bt              # Where bt is a backtrace, sets the backtrace on the exception and raises it
raise exc, "hello"              # Raises a clone of exc with the message "hello" and a new backtrace
raise StandardError, "hello"    # Raises a new instance of StandardError created with the argument "hello"

Note that an exception class's initialize method should be prepared to be invoked with 1 optional argument through the Exception::exception method, unless that class overrides its Exception::exception method for different behaviour.

Overview of Core Exceptions

The following table depicts the exception hierarchy provided by the Ruby core.

Class Description
Exception The root exception class - all objects to be raised as exceptions must inherit from this class.
>NoMemoryError Raised when memory allocation failed (out of memory)
>SystemStackError Raised when ran out of C stack or Ruby VM stack, e.g. method or proc calls nested too deep
>SecurityError Raised when Ruby security features are enabled, and a disallowed insecure operation was attempted
>SystemExit Raised to instruct the Ruby VM to terminate, typically by the Kernel#exit or Kernel#abort methods
>SignalException Raised when the Ruby VM receives an OS signal (other than the SIGINT signal) that it has been configured to raise as an exception
>>Interrupt Like SignalException, for the SIGINT signal
>ScriptError A container for exceptions due to problems with Ruby code
>LoadError Raised when the Ruby VM is unable to open a Ruby sourcefile or extension library that it is attempting to load
>SyntaxError Raised when the Ruby VM encounters invalid code when trying to compile Ruby code, e.g. a Ruby sourcefile or eval string.
>>NotImplementedError Raised when functionality is invoked which is not available or not yet supported on the current platform
>StandardError A container for "regular" exceptions, i.e. all exceptions other than the special circumstance exceptions described above. The rescue clause catches StandardError by default. Application-defined exceptions should inherit from this class.
>>ArgumentError Raised when invalid arguments were provided to a method (and there is not a more specific exception type that could be thrown instead)
>>EncodingError A container for exceptions related to problems with translating between different String character set encodings. Note that a failure due to invalid byte sequences when interpreting against a single encoding is raised as an ArgumentError.
>>>Encoding::CompatibilityError Raised when an operation requires strings to be bytecode-compatible with different character sets, and they are not
>>>Encoding::ConverterNotFoundError Raised when Ruby can't transcode between character encodings because the necessary converters are not available
>>>Encoding::InvalidByteSequenceError Raised when Ruby can't transcode between character encodings due to invalid byte sequences in the source string
>>>Encoding::UndefinedConversionError Raised when Ruby can't transcode between character encodings because the target encoding does not support a character in the source string
>>FiberError Raised for errors associated with creating and manipulating Fibers
>>IOError Raised for general errors associated with I/O operations, other than OS-raised errors which are raised as SystemCallErrors
>>>EOFError Raised for end-of-file errors associated with I/O operations
>>IndexError Raised when an invalid index was specified, e.g. into an array
>>>KeyError Raised when an invalid key was specified, e.g. into a hash table
>>>StopIteration Raised when using external (pull-style) iteration with an Enumerator object when the iteration is complete. Rescued by Kernel#loop so this method can be used as a convenience to convert from pull-style to push-style enumeration.
>>LocalJumpError Raised when a yield or control transfer instruction fails to resolve
>>Math::DomainError Raised when a math function is invoked with arguments outside its valid domain
>>NameError Raised for errors associated with identifiers specified in Ruby code`
>>>NoMethodError Raised when attempting to call a method on an object and the call is not resolved, either directly or via method_missing. method_missing is required to raise this exception when invoked with methods that it doesn't implement.
>>RangeError Raised for general errors where a numerical argument is outside the valid range for the operation
>>>FloatDomainError Raised when attempting to convert special floating-point values (such as Infininty and NaN) to other numeric types which don't support them
>>RegexpError Raised for invalid regular expressions
>>RuntimeError A generic exception class for errors not falling in other categories. This is the default exception class raised by e.g. Kernel#raise if no other exception class is specified.
>>SystemCallError A container for exception classes that represent errors returned by OS system calls
>>>Errno::... SystemCallError has a subclass for each type of OS-returned error, in the Errno module's namespace, e.g. Errno::EINVAL
>>ThreadError Raised for errors associated with creating and manipulating Threads and Mutexes
>>TypeError Raised for general errors where an argument is not of the expected class
>>ZeroDivisionError Raised when attempting to divide by 0

NoMemoryError and SystemStackError

In the course of execution, Ruby will throw an exception if certain resources are required and are not available:

  • NoMemoryError, if memory could not be allocated
  • SystemStackError, if we have run out of stack to execute a subroutine (either Ruby VM stack or underlying C machine stack)

The execution raising process is slightly modified for these exceptions:

  • A single global instance of NoMemoryError and SystemStackError is created by the Ruby VM at initialization and used henceforth. Ruby does not create new instances when raising these exceptions.
  • When the global instance of SystemStackError is raised (by the Ruby VM or otherwise):
  • During the creation phase, it will be used directly, rather than being treated as a template (i.e. .exception() will not be called), and it will not be assigned a backtrace even if requested
  • During the raising phase, it will not go through the usual backtrace/set_backtrace protocol, rather a simple string will be assigned to its internal backtrace state directly

When the Ruby VM is raising a NoMemoryError or, on some architectures, a machine-stack-related SystemStackError, the execution properties of the affected fiber are temporarily modified until the exception is actually raised - in particular while creating or raising the exception, or executing any "raise" event hooks as a result of the exception:

  • Ruby will not re-raise the same type of exception again - further resource unavailable errors may be immediately fatal to the program
  • The fiber will not do any thread interrupt processing

SystemCallError

When an OS system call returns an error code, the Ruby core uses subclasses of SystemCallError to raise an exception.

Ruby's OS error codes are in the POSIX style, defined by names such as "EINVAL". On POSIX-like operating systems, these names are directly defined by the OS API, however they are just source-code-level symbols. In code execution, the names are translated to integer numbers (such a number is called an "errno"), and this mapping to numbers may differ between different operating systems and architectures. On non-POSIX operating systems with other error code schemes (such as Windows), Ruby translates the native error codes into appropriate POSIX-style error names.

Ruby defines pre-defines an exception class for each known error name as follows:

  • A subclass of SystemCallError is created, and stored in the namespace of the top-level module Errno (e.g. Errno::EINVAL)
  • The (architecture-dependent) errno of that name is stored in the Errno constant of that class's namespace (e.g. Errno::EINVAL::Errno
  • If two error names map to the same errno, then their corresponding constants in the Errno namespace will reference the same class (e.g. on architectures where "EAGAIN" and "EWOULDBLOCK" have the same errno, Errno::EAGAIN and Errno::EWOULDBLOCK will be the same class)
  • A subclass called Errno::ENOERROR is always predefined with errno 0, and for error names which don't exist in the current architecture, their corresponding Errno constants will reference Errno::ENOERROR

In the unlikely event that an OS system call returns an errno that doesn't correspond to any name Ruby knows about, Ruby will at that point generate a subclass with a name corresponding to that number (e.g. for errno 41 the name may be Errno::E041) and raise an instance of it.

Ruby code can instantiate SystemCallError either by instantiating a subclass, or instantiating SystemCallError directly and specifying the errno. Ruby supports matching instances of either type to their corresponding subclass by way of SystemCallError::===.

Fatal exceptions

A fatal exception is something you should never see, but in case it does come up, here is the explanation.

The Ruby core and extension libraries can raise an exception flagged as fatal, which they only do if they detect an unexpected problem in their own code and wish to initiate immediate termination. Ruby code can't raise a fatal exception directly.

Fatal exceptions are similar to other exceptions except:

  • They can't be caught in rescue clauses
  • If the current thread terminates with a fatal exception, it is propagated to the main thread

Fatal exceptions therefore typically result eventually in program termination, after all pertinent ensure clauses are run and other termination activites are completed.

Throw/Catch

Ruby provides another exception-style mechanism via Kernel#throw and Kernel#catch. These are roughly analagous to raise methods and the rescue expression.

Throw/catch is used as a kind of powerful "goto" statement, very similar to "setjmp"/"longjmp" OS system calls. It works as follows:

  • You call Kernel#catch, which is analogous to a "goto" label or "setjmp" call, with an optional "tag" parameter
  • The catch "tag" is an arbitrary object that labels the catch invocation. If you don't specify one, catch will create one by allocating an empty Object instance (via its allocator function)
  • You must pass a block to the catch method, which it will invoke with the tag as a parameter
  • During the execution of the block, or of any code recursively called from within the block, you may call Kernel#throw on the tag, which will act like an exception that is raised to be caught by the catch method
  • You may pass a value to the throw method, which will be returned by the catch method. If the catch method returns normally (without catching a throw), it will return nil

throw-style exceptions can't be caught by rescue clauses, however they will cause ensure clauses to be invoked in the normal way. The tag specified in throw must be the exact same object as the tag labelling the corresponding catch. If you attempt to throw on a tag which has no corresponding catch in the stack, throw will raise a (normal) exception.

Clone this wiki locally