Skip to content
John Mair edited this page Feb 27, 2014 · 34 revisions

Pry Exception explorer

Quick Menu:

Overview

pry-exception_explorer is a plugin for the Pry REPL to intercept exceptions in your code. Once inside the session you have the opportunity to uncover the cause of an 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.

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

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 argument. 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.inline! 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.inline!
EE.intercept(ArgumentError)

begin
  raise ArgumentError
rescue ArgumentError
end

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

Internal exceptions such as NoMethodErrors and NameErrors can be intercepted.

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