Question

Is there an idiomatic way to catch an exception when multiple methods may throw?:

try:
    someMethod()  # raises OSError
    someOtherMethod()  # raises OSError
except OSError:
    handle()

The exception may or may not be caused by the same event internal to the helper functions, so in the current layout it seems impossible to discern which method threw the exception. To resolve, there are two cases I could implement.

The first case is wrapping each method in its own try/except:

try:
    someMethod()
except OSError:
    handle()

try:
    someOtherMethod()
except OSError:
    handle()

While this address the issue of identifying which helper function threw the exception, it presents a new problem. If methods require successful of preceding methods, separate try/except blocks lead to forced early returning as subsequent blocks should not be executed:

try:
    someMethod()
except OSError:
    handle()
    return

try:
    someOtherMethod()
except OSError:
    handle()

This seems like a code-smell and is therefore undesired. The second case is creating custom exceptions based on the content of the internally generated exception:

class FailedReadIOError(Exception):
    def __init__(self, msg):
        super().__init__(msg)

class FailedFolderError(Exception):
    def __init__(self, msg):
        super().__init__(msg)

def someMethod():

    try:
        # stuff
    except OSError as err:
        raise FailedReadIOError('thrown in someMethod') from err

    try:
        # stuff
    except OSError as err:
        raise FailedFolderError('thrown in someMethod') from err

def someOtherMethod():

    try:
        # stuff
    except OSError as err:
        raise FailedReadIOError('thrown in someOtherMethod') from err


try:
    someMethod()
    someOtherMethod()
except FailedReadIOError:
    handle_this()
except FailedFolderError:
    handle_that()

This adequately addresses the concerns but requires additional (minimal) code to be written. Is there a pattern that achieves the same result without the need to create custom exceptions when multiple methods throw the same exception?

Was it helpful?

Solution

This depends entirely how you will handle the errors.

  • If you can't handle the errors meaningfully, don't.
  • If your functions raise errors that can be meaningfully handled, then the errors should be meaningful.

Often, no error handling is the most appropriate approach. In many contexts, just letting errors propagate can be totally acceptable.

Sometimes, a broad error category like OSError is sufficient to understand what has gone wrong and how your code can recover. Depending on context, inspecting the error's errno could yield further information. Such a strategy might be suitable for a function that wraps a lower-level function. But this has the implication that the other code needs to understand the OSError appropriately.

But in general, there is no good way around defining custom exception types that communicate the problem in a manner that is suitable in the context of your business logic. If your code needs to treat a failed read differently from a missing folder, your code should have a clear representation of these concepts. Any program has some kind of data model. We can use classes to structure this model, and to give names to certain concepts. Exceptions are a part of this model just like other classes.


While the above is nice theory, the most practical approach might be to maintain extra state variables that let us understand where the exception originated from:

checkpoint = 'start'
try:
    someMethod()  # raises OSError
    checkpoint = 'inner'
    someOtherMethod()  # raises OSError
except OSError as ex:
    if checkpoint == 'start':
      handle_this(ex)
    else:
      handle_that(ex)

A similar idea would be to inspect the exceptions stack trace to determine which function caused it…

While such techniques are not generally a good idea because such state or such dependency on internal functions can make the code massively more complex to maintain, it can be appropriate on a small scale, e.g. scripts or in code that does not depend on other modules.

Licensed under: CC-BY-SA with attribution
scroll top