I have here an approach for chaining exceptions in case a lower layer (library) raises an exception which is caught in an upper layer (application) and later given as cause when a different exception is raised. Passing this cause exception is meant to offer access to the stack trace of the inner exception for debugging.
This approach is implemented in Python 3 and in Java, so it definitely makes sense; you also quickly find questions on Stackoverflow concerning it.
I even extended this feature by not only using chains of exceptions but also trees. Trees, why trees? Because I had situations in which my application layer tried various approaches using the library layer. If all failed (raised an exception), my application layer also raised an exception; this is the case in which I wanted to pass more than one cause exception into my own exception (hence the tree of causes).
My approach uses a special Exception class from which all my exceptions will inherit; standard exception must be wrapped (directly after catching, to preserve the exception stack trace). Please see the examples contained in the code below. The exception itself is rather small.
I'd be happy to hear any comments regarding memory leaks (I didn't find any but one never knows), usability, enhancements or similar.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | #!/usr/bin/env python
import traceback
import re
import sys
class CausedException(Exception):
def __init__(self, *args, **kwargs):
if len(args) == 1 and not kwargs and isinstance(args[0], Exception):
# we shall just wrap a non-caused exception
self.stack = (
traceback.format_stack()[:-2] +
traceback.format_tb(sys.exc_info()[2]))
# ^^^ let's hope the information is still there; caller must take
# care of this.
self.wrapped = args[0]
self.cause = ()
super(CausedException, self).__init__(repr(args[0]))
# ^^^ to display what it is wrapping, in case it gets printed or similar
return
self.wrapped = None
self.stack = traceback.format_stack()[:-1] # cut off current frame
try:
cause = kwargs['cause']
del kwargs['cause']
except:
cause = ()
self.cause = cause if isinstance(cause, tuple) else (cause,)
super(CausedException, self).__init__(*args, **kwargs)
def causeTree(self, indentation=' ', alreadyMentionedTree=[]):
yield "Traceback (most recent call last):\n"
ellipsed = 0
for i, line in enumerate(self.stack):
if (ellipsed is not False and i < len(alreadyMentionedTree) and
line == alreadyMentionedTree[i]):
ellipsed += 1
else:
if ellipsed:
yield " ... (%d frame%s repeated)\n" % (
ellipsed, "" if ellipsed == 1 else "s")
ellipsed = False # marker for "given out"
yield line
exc = self if self.wrapped is None else self.wrapped
for line in traceback.format_exception_only(exc.__class__, exc):
yield line
if self.cause:
yield ("caused by: %d exception%s\n" %
(len(self.cause), "" if len(self.cause) == 1 else "s"))
for causePart in self.cause:
for line in causePart.causeTree(indentation, self.stack):
yield re.sub(r'([^\n]*\n)', indentation + r'\1', line)
def write(self, stream=None, indentation=' '):
stream = sys.stderr if stream is None else stream
for line in self.causeTree(indentation):
stream.write(line)
if __name__ == '__main__':
def deeplib(i):
if i == 3:
1 / 0 # raise non-caused exception
else:
raise CausedException("deeplib error %d" % i)
def library(i):
if i == 0:
return "no problem"
elif i == 1:
raise CausedException("lib error one %d" % i)
elif i == 2:
try:
deeplib(i)
except CausedException, e:
raise CausedException("lib error two %d" % i, cause=e)
except Exception, e: # non-caused exception?
raise CausedException("lib error two %d" % i,
cause=CausedException(e)) # wrap non-caused exception
elif i == 3:
try:
deeplib(i)
except CausedException, e:
raise CausedException("lib error three %d" % i, cause=e)
except Exception, e: # non-caused exception?
wrappedException = CausedException(e) # wrap it for fitting in
try:
deeplib(i-1) # try again
except CausedException, e:
raise CausedException("lib error three %d" % i,
cause=(wrappedException, CausedException(e)))
else:
raise CausedException("lib error unexpected %d" % i)
def application():
e0 = e1 = e2 = e3 = None
try: library(0)
except CausedException, e: e0 = e
try: library(1)
except CausedException, e: e1 = e
try: library(2)
except CausedException, e: e2 = e
try: library(3)
except CausedException, e: e3 = e
if e0 or e1 or e2 or e3:
raise CausedException("application error",
cause=tuple(e for e in (e0, e1, e2, e3) if e is not None))
try:
application()
except CausedException, e:
e.write()
print >>sys.stderr, "NOW WITH MORE OBVIOUS INDENTATION"
e.write(indentation='|| ')
print >>sys.stderr, "NOW THE DEFAULT HANDLER"
application()
|
The trace for the nested exception in the example above will look like this: