Skip to content

Instantly share code, notes, and snippets.

@stanio
Last active September 7, 2017 13:12
Show Gist options
  • Save stanio/13d74294ca1868fed7fb to your computer and use it in GitHub Desktop.
Save stanio/13d74294ca1868fed7fb to your computer and use it in GitHub Desktop.
Formatting function to provide full exception chain backtrace
# frozen_string_literal: true
#
# This module, both source code and documentation,
# is in the Public Domain, and comes with NO WARRANTY.
#
require 'stringio'
module ExceptionFormatter
module_function
#
# Employs the Ruby 2.1 <code>Exception#cause</code> to provide backtrace of
# the complete exception chain.
#
# Given the following test:
#
# require_relative 'exception_formatter'
# include ExceptionFormatter
#
# def lower_level; raise 'The ultimate lower cause' end
# def level_one; lower_level rescue raise 'Exception reported uniformly for level 1' end
# def level_two; level_one end # Exception propagated from level 2
# def level_three; level_two rescue raise 'Exception reported uniformly for level 3' end
#
# begin
# level_three
# rescue => e
# puts format_backtrace(e)
# end
#
# the following output is produced:
#
# RuntimeError: Exception reported uniformly for level 3
# test.rb:7:in `rescue in level_three'
# test.rb:7:in `level_three'
# test.rb:10:in `<main>'
# Caused by: RuntimeError: Exception reported uniformly for level 1
# test.rb:5:in `rescue in level_one'
# test.rb:5:in `level_one'
# test.rb:6:in `level_two'
# ... 2 more
# Caused by: RuntimeError: The ultimate lower cause
# test.rb:4:in `lower_level'
# ... 4 more
#
# Changing the test like:
#
# require_relative 'exception_formatter'
# include ExceptionFormatter
#
# def lower_level; raise 'The ultimate lower cause' end
# def level_one; lower_level end # Exception propagated from level 1
# def level_two; level_one rescue raise 'Exception reported uniformly for level 2' end
# def level_three; level_two rescue raise 'Exception reported uniformly for level 3' end
#
# begin
# level_three
# rescue => e
# format_backtrace(e, $stderr)
# end
#
# produces:
#
# RuntimeError: Exception reported uniformly for level 3
# test2.rb:7:in `rescue in level_three'
# test2.rb:7:in `level_three'
# test2.rb:10:in `<main>'
# Caused by: RuntimeError: Exception reported uniformly for level 2
# test2.rb:6:in `rescue in level_two'
# test2.rb:6:in `level_two'
# ... 2 more
# Caused by: RuntimeError: The ultimate lower cause
# test2.rb:4:in `lower_level'
# test2.rb:5:in `level_one'
# ... 3 more
#
# That is, only the top-level exception trace is shown in full, and traces
# of nested exceptions is filtered to show just their unique parts (not
# repeating redundant stacks).
#
# @param exception [Exception] exception to format
# @param io [IO] output stream to write the result to. If not given the
# result is returned as a String
# @return [String] if no output stream given – formatted backtrace.
# @return [Void] if output stream given for the result.
# @see Exception#cause
#
def format_backtrace(exception, io = nil)
out = io || StringIO.new
_format_single(out, exception)
current_backtrace = exception.backtrace
cause = exception.cause
until cause.nil? do
cause_trace = _unique_trace(cause.backtrace.to_a, current_backtrace)
out.print 'Caused by: '
_format_single(out, cause, cause_trace)
backtrace_length = cause.backtrace.length
out.puts "\t... #{backtrace_length - cause_trace.length} more" \
if backtrace_length > cause_trace.length
current_backtrace = cause.backtrace
cause = cause.cause
end
out.string if io.nil?
end
def _format_single(out, exception, backtrace=nil)
out.puts "#{exception.class}: #{exception.message}"
backtrace ||= exception.backtrace.to_a
backtrace.each { |trace| out.puts "\t#{trace}" }
end
private_class_method :_format_single
def _unique_trace(backtrace1, backtrace2)
i = 1
while i <= backtrace1.size && i <= backtrace2.size
break if backtrace1[-i] != backtrace2[-i]
i += 1
end
backtrace1[0..-i]
end
private_class_method :_unique_trace
end
require_relative 'exception_formatter'
include ExceptionFormatter
def lower_level; raise 'The ultimate lower cause' end
def level_one; lower_level rescue raise 'Exception reported uniformly for level 1' end
def level_two; level_one end # Exception propagated from level 2
def level_three; level_two rescue raise 'Exception reported uniformly for level 3' end
begin
level_three
rescue => e
puts format_backtrace(e)
end
require_relative 'exception_formatter'
include ExceptionFormatter
def lower_level; raise 'The ultimate lower cause' end
def level_one; lower_level end # Exception propagated from level 1
def level_two; level_one rescue raise 'Exception reported uniformly for level 2' end
def level_three; level_two rescue raise 'Exception reported uniformly for level 3' end
begin
level_three
rescue => e
format_backtrace(e, $stderr)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment