Skip to content

Exceptions and Throw

datanorris edited this page Jul 7, 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, 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.

Special exceptions for unavailable resources

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

Fatal exceptions throw/catch accessing errinfo from ruby code

  • 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 - e.g. methods, variables or constants which are invalid, don't exist or are disallowed in the context (excluding those raised for NoMethodError)
      • 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
      • Errno::*
      • Errno::NOERROR
    • 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

Clone this wiki locally