Welcome, guest | Sign In | My Account | Store | Cart

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.

Python, 116 lines
  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()

1 comment

Alfe (author) 9 years, 2 months ago  # | flag

The trace for the nested exception in the example above will look like this:

Traceback (most recent call last):
  File "src/exception_chaining.py", line 108, in <module>
    application()
  File "src/exception_chaining.py", line 105, in application
    cause=tuple(e for e in (e0, e1, e2, e3) if e is not None))
CausedException: application error
caused by: 3 exceptions
  Traceback (most recent call last):
    ... (1 frame repeated)
    File "src/exception_chaining.py", line 97, in application
      try: library(1)
    File "src/exception_chaining.py", line 69, in library
      raise CausedException("lib error one %d" % i)
  CausedException: lib error one 1
  Traceback (most recent call last):
    ... (1 frame repeated)
    File "src/exception_chaining.py", line 99, in application
      try: library(2)
    File "src/exception_chaining.py", line 74, in library
      raise CausedException("lib error two %d" % i, cause=e)
  CausedException: lib error two 2
  caused by: 1 exception
    Traceback (most recent call last):
      ... (2 frames repeated)
      File "src/exception_chaining.py", line 72, in library
        deeplib(i)
      File "src/exception_chaining.py", line 63, in deeplib
        raise CausedException("deeplib error %d" % i)
    CausedException: deeplib error 2
  Traceback (most recent call last):
    ... (1 frame repeated)
    File "src/exception_chaining.py", line 101, in application
      try: library(3)
    File "src/exception_chaining.py", line 89, in library
      cause=(wrappedException, CausedException(e)))
  CausedException: lib error three 3
  caused by: 2 exceptions
    Traceback (most recent call last):
      ... (2 frames repeated)
      File "src/exception_chaining.py", line 80, in library
        deeplib(i)
      File "src/exception_chaining.py", line 61, in deeplib
        1 / 0  # raise non-caused exception
    ZeroDivisionError: integer division or modulo by zero
    Traceback (most recent call last):
      ... (2 frames repeated)
      File "src/exception_chaining.py", line 86, in library
        deeplib(i-1)  # try again
      File "src/exception_chaining.py", line 63, in deeplib
        raise CausedException("deeplib error %d" % i)
    CausedException: deeplib error 2