diff options
Diffstat (limited to 'doc/language/exceptions.md')
| -rw-r--r-- | doc/language/exceptions.md | 521 |
1 files changed, 521 insertions, 0 deletions
diff --git a/doc/language/exceptions.md b/doc/language/exceptions.md new file mode 100644 index 0000000000..5f8f0ece69 --- /dev/null +++ b/doc/language/exceptions.md @@ -0,0 +1,521 @@ +# Exceptions + +Ruby code can raise exceptions. + +Most often, a raised exception is meant to alert the running program +that an unusual (i.e., _exceptional_) situation has arisen, +and may need to be handled. + +Code throughout the Ruby core, Ruby standard library, and Ruby gems generates exceptions +in certain circumstances: + +```rb +File.open('nope.txt') # Raises Errno::ENOENT: "No such file or directory" +``` + +## Raised Exceptions + +A raised exception transfers program execution, one way or another. + +### Unrescued Exceptions + +If an exception not _rescued_ +(see [Rescued Exceptions](#label-Rescued+Exceptions) below), +execution transfers to code in the Ruby interpreter +that prints a message and exits the program (or thread): + +```console +$ ruby -e "raise" +-e:1:in '<main>': unhandled exception +``` + +### Rescued Exceptions + +An <i>exception handler</i> may determine what is to happen +when an exception is raised; +the handler may _rescue_ an exception, +and may prevent the program from exiting. + +A simple example: + +```rb +begin + raise 'Boom!' # Raises an exception, transfers control. + puts 'Will not get here.' +rescue + puts 'Rescued an exception.' # Control transferred to here; program does not exit. +end +puts 'Got here.' +``` + +Output: + +``` +Rescued an exception. +Got here. +``` + +An exception handler has several elements: + +| Element | Use | +|-----------------------------|------------------------------------------------------------------------------------------| +| Begin clause. | Begins the handler and contains the code whose raised exception, if any, may be rescued. | +| One or more rescue clauses. | Each contains "rescuing" code, which is to be executed for certain exceptions. | +| Else clause (optional). | Contains code to be executed if no exception is raised. | +| Ensure clause (optional). | Contains code to be executed whether or not an exception is raised, or is rescued. | +| <tt>end</tt> statement. | Ends the handler. ` | + +#### Begin Clause + +The begin clause begins the exception handler: + +- May start with a `begin` statement; + see also [Begin-Less Exception Handlers](#label-Begin-Less+Exception+Handlers). +- Contains code whose raised exception (if any) is covered + by the handler. +- Ends with the first following `rescue` statement. + +#### Rescue Clauses + +A rescue clause: + +- Starts with a `rescue` statement. +- Contains code that is to be executed for certain raised exceptions. +- Ends with the first following `rescue`, + `else`, `ensure`, or `end` statement. + +##### Rescued Exceptions + +A `rescue` statement may include one or more classes +that are to be rescued; +if none is given, StandardError is assumed. + +The rescue clause rescues both the specified class +(or StandardError if none given) or any of its subclasses; +see [Built-In Exception Class Hierarchy](rdoc-ref:Exception@Built-In+Exception+Class+Hierarchy). + +```rb +begin + 1 / 0 # Raises ZeroDivisionError, a subclass of StandardError. +rescue + puts "Rescued #{$!.class}" +end +``` + +Output: + +``` +Rescued ZeroDivisionError +``` + +If the `rescue` statement specifies an exception class, +only that class (or one of its subclasses) is rescued; +this example exits with a ZeroDivisionError, +which was not rescued because it is not ArgumentError or one of its subclasses: + +```rb +begin + 1 / 0 +rescue ArgumentError + puts "Rescued #{$!.class}" +end +``` + +A `rescue` statement may specify multiple classes, +which means that its code rescues an exception +of any of the given classes (or their subclasses): + +```rb +begin + 1 / 0 +rescue FloatDomainError, ZeroDivisionError + puts "Rescued #{$!.class}" +end +``` + +##### Multiple Rescue Clauses + +An exception handler may contain multiple rescue clauses; +in that case, the first clause that rescues the exception does so, +and those before and after are ignored: + +```rb +begin + Dir.open('nosuch') +rescue Errno::ENOTDIR + puts "Rescued #{$!.class}" +rescue Errno::ENOENT + puts "Rescued #{$!.class}" +end +``` + +Output: + +``` +Rescued Errno::ENOENT +``` + +##### Capturing the Rescued \Exception + +A `rescue` statement may specify a variable +whose value becomes the rescued exception +(an instance of Exception or one of its subclasses: + +```rb +begin + 1 / 0 +rescue => x + puts x.class + puts x.message +end +``` + +Output: + +``` +ZeroDivisionError +divided by 0 +``` + +##### Global Variables + +Two read-only global variables always have `nil` value +except in a rescue clause; +they're: + +- `$!`: contains the rescued exception. +- `$@`: contains its backtrace. + +Example: + +```rb +begin + 1 / 0 +rescue + p $! + p $@ +end +``` + +Output: + +``` +#<ZeroDivisionError: divided by 0> +["t.rb:2:in 'Integer#/'", "t.rb:2:in '<main>'"] +``` + +##### Cause + +In a rescue clause, the method Exception#cause returns the previous value of `$!`, +which may be `nil`; +elsewhere, the method returns `nil`. + +Example: + +```rb +begin + raise('Boom 0') +rescue => x0 + puts "Exception: #{x0.inspect}; $!: #{$!.inspect}; cause: #{x0.cause.inspect}." + begin + raise('Boom 1') + rescue => x1 + puts "Exception: #{x1.inspect}; $!: #{$!.inspect}; cause: #{x1.cause.inspect}." + begin + raise('Boom 2') + rescue => x2 + puts "Exception: #{x2.inspect}; $!: #{$!.inspect}; cause: #{x2.cause.inspect}." + end + end +end +``` + +Output: + +``` +Exception: #<RuntimeError: Boom 0>; $!: #<RuntimeError: Boom 0>; cause: nil. +Exception: #<RuntimeError: Boom 1>; $!: #<RuntimeError: Boom 1>; cause: #<RuntimeError: Boom 0>. +Exception: #<RuntimeError: Boom 2>; $!: #<RuntimeError: Boom 2>; cause: #<RuntimeError: Boom 1>. +``` + +#### Else Clause + +The `else` clause: + +- Starts with an `else` statement. +- Contains code that is to be executed if no exception is raised in the begin clause. +- Ends with the first following `ensure` or `end` statement. + +```rb +begin + puts 'Begin.' +rescue + puts 'Rescued an exception!' +else + puts 'No exception raised.' +end +``` + +Output: + +``` +Begin. +No exception raised. +``` + +#### Ensure Clause + +The ensure clause: + +- Starts with an `ensure` statement. +- Contains code that is to be executed + regardless of whether an exception is raised, + and regardless of whether a raised exception is handled. +- Ends with the first following `end` statement. + +```rb +def foo(boom: false) + puts 'Begin.' + raise 'Boom!' if boom +rescue + puts 'Rescued an exception!' +else + puts 'No exception raised.' +ensure + puts 'Always do this.' +end + +foo(boom: true) +foo(boom: false) +``` + +Output: + +``` +Begin. +Rescued an exception! +Always do this. +Begin. +No exception raised. +Always do this. +``` + +#### End Statement + +The `end` statement ends the handler. + +Code following it is reached only if any raised exception is rescued. + +#### Begin-Less \Exception Handlers + +As seen above, an exception handler may be implemented with `begin` and `end`. + +An exception handler may also be implemented as: + +- A method body: + + ```rb + def foo(boom: false) # Serves as beginning of exception handler. + puts 'Begin.' + raise 'Boom!' if boom + rescue + puts 'Rescued an exception!' + else + puts 'No exception raised.' + end # Serves as end of exception handler. + ``` + +- A block: + + ```rb + Dir.chdir('.') do |dir| # Serves as beginning of exception handler. + raise 'Boom!' + rescue + puts 'Rescued an exception!' + end # Serves as end of exception handler. + ``` + +#### Re-Raising an \Exception + +It can be useful to rescue an exception, but allow its eventual effect; +for example, a program can rescue an exception, log data about it, +and then "reinstate" the exception. + +This may be done via the `raise` method, but in a special way; +a rescuing clause: + + - Captures an exception. + - Does whatever is needed concerning the exception (such as logging it). + - Calls method `raise` with no argument, + which raises the rescued exception: + +```rb +begin + 1 / 0 +rescue ZeroDivisionError + # Do needful things (like logging). + raise # Raised exception will be ZeroDivisionError, not RuntimeError. +end +``` + +Output: + +``` +ruby t.rb +t.rb:2:in 'Integer#/': divided by 0 (ZeroDivisionError) + from t.rb:2:in '<main>' +``` + +#### Retrying + +It can be useful to retry a begin clause; +for example, if it must access a possibly-volatile resource +(such as a web page), +it can be useful to try the access more than once +(in the hope that it may become available): + +```rb +retries = 0 +begin + puts "Try ##{retries}." + raise 'Boom' +rescue + puts "Rescued retry ##{retries}." + if (retries += 1) < 3 + puts 'Retrying' + retry + else + puts 'Giving up.' + raise + end +end +``` + +``` +Try #0. +Rescued retry #0. +Retrying +Try #1. +Rescued retry #1. +Retrying +Try #2. +Rescued retry #2. +Giving up. +# RuntimeError ('Boom') raised. +``` + +Note that the retry re-executes the entire begin clause, +not just the part after the point of failure. + +## Raising an \Exception + +Method Kernel#raise raises an exception. + +## Custom Exceptions + +To provide additional or alternate information, +you may create custom exception classes. +Each should be a subclass of one of the built-in exception classes +(commonly StandardError or RuntimeError); +see [Built-In Exception Class Hierarchy](rdoc-ref:Exception@Built-In+Exception+Class+Hierarchy). + +```rb +class MyException < StandardError; end +``` + +## Messages + +Every `Exception` object has a message, +which is a string that is set at the time the object is created; +see Exception.new. + +The message cannot be changed, but you can create a similar object with a different message; +see Exception#exception. + +This method returns the message as defined: + +- Exception#message. + +Two other methods return enhanced versions of the message: + +- Exception#detailed_message: adds exception class name, with optional highlighting. +- Exception#full_message: adds exception class name and backtrace, with optional highlighting. + +Each of the two methods above accepts keyword argument `highlight`; +if the value of keyword `highlight` is `true`, +the returned string includes bolding and underlining ANSI codes (see below) +to enhance the appearance of the message. + +Any exception class (Ruby or custom) may choose to override either of these methods, +and may choose to interpret keyword argument <tt>highlight: true</tt> +to mean that the returned message should contain +[ANSI codes](https://en.wikipedia.org/wiki/ANSI_escape_code) +that specify color, bolding, and underlining. + +Because the enhanced message may be written to a non-terminal device +(e.g., into an HTML page), +it is best to limit the ANSI codes to these widely-supported codes: + +- Begin font color: + + | Color | ANSI Code | + |---------|------------------| + | Red | <tt>\\e[31m</tt> | + | Green | <tt>\\e[32m</tt> | + | Yellow | <tt>\\e[33m</tt> | + | Blue | <tt>\\e[34m</tt> | + | Magenta | <tt>\\e[35m</tt> | + | Cyan | <tt>\\e[36m</tt> | + +<br> + +- Begin font attribute: + + | Attribute | ANSI Code | + |-----------|-----------------| + | Bold | <tt>\\e[1m</tt> | + | Underline | <tt>\\e[4m</tt> | + +<br> + +- End all of the above: + + | Color | ANSI Code | + |-------|-----------------| + | Reset | <tt>\\e[0m</tt> | + +It's also best to craft a message that is conveniently human-readable, +even if the ANSI codes are included "as-is" +(rather than interpreted as font directives). + +## Backtraces + +A _backtrace_ is a record of the methods currently +in the [call stack](https://en.wikipedia.org/wiki/Call_stack); +each such method has been called, but has not yet returned. + +These methods return backtrace information: + +- Exception#backtrace: returns the backtrace as an array of strings or `nil`. +- Exception#backtrace_locations: returns the backtrace as an array + of Thread::Backtrace::Location objects or `nil`. + Each Thread::Backtrace::Location object gives detailed information about a called method. + +By default, Ruby sets the backtrace of the exception to the location where it +was raised. + +The developer might adjust this by either providing `backtrace` argument +to Kernel#raise, or using Exception#set_backtrace. + +Note that: + +- by default, both `backtrace` and `backtrace_locations` represent the same backtrace; +- if the developer sets the backtrace by one of the above methods to an array of + Thread::Backtrace::Location, they still represent the same backtrace; +- if the developer sets the backtrace to a string or an array of strings: + - by Kernel#raise: `backtrace_locations` become `nil`; + - by Exception#set_backtrace: `backtrace_locations` preserve the original + value; +- if the developer sets the backtrace to `nil` by Exception#set_backtrace, + `backtrace_locations` preserve the original value; but if the exception is then + reraised, both `backtrace` and `backtrace_locations` become the location of reraise. |
