Skip to content
banister edited this page Feb 14, 2012 · 34 revisions

Pry Exception explorer

Quick Menu:

Overview

pry-exception_explorer is a plugin for the Pry REPL designed to intercept exceptions in your code. Once inside the session you have an opportunity to uncover the cause of the error and even to correct it, before letting your program continue. It is based on similar error consoles found in the Lisp and Smalltalk worlds.

Back to the top

EE shortcut

As an alternative to using the full module name PryExceptionExplorer, you can use the constant EE instead. They both refer to the same object, but the abbreviated form may make programs more concise.

Back to the top

The intercept method

This method allows the user to define the situations where an exception interception occurs. This method can be invoked in two ways.

1. Block-form

The general form takes a block, the block is passed both the frame where the exception was raised, and the exception itself. The user then creates an assertion (a stack-assertion) based on these attributes. If the assertion is later satisfied by a raised exception, that exception will be intercepted.

The first parameter yielded to the block is a PryExceptionExplorer::LazyFrame instance ( representing the context where the exception was raised), the second is the exception object.

The PryExceptionExplorer::LazyFrame instance supports a number of methods that expose details of the stack frame:

  • self the self of the stack frame.
  • method_name the method name (as a Symbol) of the frame's method (if it exists).
  • klass the class of the frame's self
  • prev the previous stack frame (as another instance of LazyFrame).

The assertion is known as a stack assertion as the user can assert on the state of any frame in the call-stack, selecting the precise frame they want by using the LazyFrame#prev method.

Example: Asserting that the exception must be a RuntimeError and the frame method is alpha and its caller beta:

EE.intercept do |frame, ex|
  ex.is_a?(RuntimeError) && frame.method_name == :alpha
  && frame.prev.method_name == :beta
end

2. Exception classes

In the second form, the method simply takes an exception class, or a number of exception classes. If one of these exceptions is raised, it will be intercepted.

Example: Intercept all ArgumentError and NoMethodError exceptions

EE.intercept(ArgumentError, NoMethodError)

Back to the top

Recovering from exceptions

When you are in the context of an exception you can investigate the cause of the error by inspecting the contents of variables and analysing code. You can even change the values of variables (local or otherwise).

This doesn't just apply to the exception context, you can change the value of variables in parent frames too. While in the session if you modify state such that you've fixed the cause of the exception you can then safely continue your program by entering the continue-exception command - your program should continue as normal as if no exception occurred.

Note that if you simply exit a session via the exit command or by pressing ^D the exception will go on to be raised properly.

Example: Fixing an exception state in a parent frame and continuing the program with continue-exception

[3] (pry) main: 0> hello
RuntimeError: Error, did not receive a String!
from (pry):7:in `check_for_string'
[4] (pry) main: 0> enter-exception

Frame number: 0/14
Frame type: method

From: (pry) @ line 7 in Object#check_for_string:

    2:   x = 5
    3:   check_for_string(x)
    4:   puts "got x, confirmed that it is the string: #{x}"
    5: end
    6: def check_for_string(var)
 => 7:   raise "Error, did not receive a String!" if !var.is_a?(String)
    8: end
    9: hello

[5] (pry) main: 0> up

Frame number: 1/14
Frame type: method

From: (pry) @ line 3 in Object#hello:

    1: def hello
    2:   x = 5
 => 3:   check_for_string(x)
    4:   puts "got x, confirmed that it is the string: #{x}"
    5: end
    6: def check_for_string(var)
    7:   raise "Error, did not receive a String!" if !var.is_a?(String)
    8: end

[6] (pry) main: 0> x = "john"
=> "john"
[7] (pry) main: 0> continue-exception
got x, confirmed that it is the string: john

Back to the top

The skip_until and skip_while decorators

The EE.intercept method returns an instance of PryExceptionExplorer::Intercept, onto this we can optionally chain the skip, skip_until and skip_while methods. These methods determine where in the call-stack the Pry session will start.

This can be useful when an exception is raised fairly deep down in code but a session at a parent stack frame could be more useful to the user. See the Plymouth project for an interesting application of this.

Example: Start the session on nearest stack frame with the method name run

EE.intercept(ArgumentError).skip_until { |frame| frame.method_name == :run }

Back to the top

Intercepting exceptions at the raise-site

When you require pry-exception_explorer in your program it is disabled by default. Once you enable it using EE.enable! it will automatically intercept exceptions at the point the raise method is called, regardless of whether that exception is properly handled (rescued) later on in the code.

In the example below we configure EE to intercept every ArgumentError, at the point the raise occurs:

Example: This will be intercepted even though it's 'rescued'

require 'pry-exception_explorer'

EE.enable!
EE.intercept(ArgumentError)

begin
  raise ArgumentError
rescue ArgumentError
end

Back to the top

Intercepting only escaped exceptions

If you do not want to intercept exceptions at the point they're raised, you can use the EE.wrap method to enclose a piece of code in a block and only intercept the exceptions that bubble out of it.

Example: Will not be intercepted

require 'pry-exception_explorer'

EE.intercept(ArgumentError)

EE.wrap do
  begin
    raise ArgumentError
  rescue ArgumentError
  end
end

Example: Will be intercepted

require 'pry-exception_explorer'

EE.intercept(ArgumentError)
EE.wrap { raise ArgumentError }

Back to the top

Intercepting only exceptions that bubble out of your program

If you prefer to intercept only those exceptions that would bubble out of your program (killing your program in the process) you can invoke the pry executable with the -w switch. This causes the pry-exception_explorer plugin to wrap your entire program in an EE.wrap and intercept any exception that escapes.

Note that to tune the exceptions intercepted in this way via the EE.intercept method you will need to require pry-exception_explorer inside your project's local .pryrc and define the EE.intercept method there.

Example: Intercepting all exceptions that bubble out of a program

crow:pry john$ pry -w my_program.rb

Back to the top

Using pry-exception_explorer inside Pry itself

When Pry is started with the pry executable the pry-exception_explorer plugin is active. However, when an exception is raised inside the Pry REPL it will not automatically cause a session to start in the exception context. Instead you have to explicitly type the command enter-exception to enter the context of the most recent exception. This is because you are already in an interactive session and so you can be more selective over exactly which exceptions you wish to enter. To exit an exception and return to your previous Pry context use the exit-exception command.

[3] (pry) main: 0> hello
RuntimeError: yo
from (pry):5:in `goodbye'
[4] (pry) main: 0> enter-exception

Frame number: 0/14
Frame type: method

From: (pry) @ line 5 in Object#goodbye:

    1: def hello
    2:   goodbye
    3: end
    4: def goodbye
 => 5:   raise "yo"
    6: end
    7: hello

[5] (pry) main: 0>

Back to the top

Intercepting C-level exceptions

pry-stack_explorer has limited and very experimental support for intercepting C-level exceptions. A C-level exception is one that is not explicitly raised from a Ruby program using the raise method, for example the code 1/0 generates a C-level ZeroDivisionError exception.

To activate the C exception support start the Pry executable with the --c-exceptions flag. Unfortunately this has to be done when starting the Pry executable, it cannot be done at runtime.

Note that you may get strange warnings and/or error messages (usually related to a symbol called _environ) when using the C exception support, this is just symptomatic of the experimental nature of the support. Usually the C intercept functionality still mostly works regardless of the warnings and error messages.

Also note that even with the C exception support turned on, some C exceptions are still not intercepted. This is definitely an area that will be improved on in the future.

Example: Hook the 1/0 exception

[2] (pry) main: 0> hello
ZeroDivisionError: hooked exception (pry)
from (pry):2:in `/'
[3] (pry) main: 0> enter-exception

Frame number: 0/13
Frame type: method

From: (pry) @ line 2 in Object#hello:

    1: def hello
 => 2:   1/0
    3: end
    4: hello

[4] (pry) main: 0> 

Back to the top

Clone this wiki locally