How to properly propagate error messages using Python

Viewed 69

I am kinda new to proper Python error handling and I struggle finding the beast way how to handle errors in the chain of several methods.

I have 3 methods - a, b, c. a is calling b and b is calling c. How to propagate the error from the method c back to the method a so I can e.g. store somewhere it or return during API response?

Example code:

def c(x, y):
  try:
    return int(x), int(y)
  except Exception:
    print("x or y is probably not a number")

def b(x, y):
  try:
    x, y = c(x, y)
    return x + y
  except Exception:
    print("issue during sum of x and y")

def a(x, y):
  try:
    return b(x, y)
  except Exception:
    print("some unknown error occured")

 
result = a(4, 5)
result = a('test', 10)

The code above prints some errors under some conditions. It's pretty clear that you can see those errors in the console, but how to work with those 'error' messages later? For example I would like to return them and store if method a is called from another module. Right now the result has value of None, in case of an error occurency.

In other words, I would like to "somehow" jump from method c directly back into method a and display the response.

Is the proper way to return the error message itself like this?

def c(x, y):
  try:
    return int(x), int(y)
  except Exception:
    return "x or y is probably not a number"
1 Answers

error propagation

If you want to propagate the exception you can use use from clause

all the information ca be check here https://docs.python.org/3/reference/simple_stmts.html#grammar-token-raise-stmt

def c(x, y):
    try:
        return int(x), int(y)
    except Exception as e:   
        msg = "x or y is probably not a number"
        raise Exception(msg) from e   

def b(x, y):
    try:
        x, y = c(x, y)
        return x + y 
    except Exception as e:
        msg = "issue during sum of x and y"
        raise Exception(msg) from e   

def a(x, y):
    try:
        return b(x, y)
    except Exception as e:
        msg = "some unknown error occured"
        raise Exception(msg) from e    

result = a('test', 10)

That way you have tracked all way back all errors and their origins

this is the output: https://i.stack.imgur.com/2ho5K.png

supresion of traceback

in case you want to supress all the traceback of the error you need to use from None

def a(x, y):
    try:
        return b(x, y)
    except Exception as e:
        msg = "some unknown error occured"
        raise Exception(msg) from None   

Now running the code will supress all previos errors, this is the output: https://i.stack.imgur.com/loFis.png

Recovering the error as a string

The best way to capture the error I found so far and thanks to the comments of @anton who suggested using traceback is using a decorator

from functools import wraps, partial
import traceback  
import sys


def error_handler(func=None, *, raise_error=False):
    if not func:
        return partial(error_handler, raise_error=raise_error)
    
    @wraps(func)
    def func_exectutor(*args, **kwargs):
        try:
            out = func(*args, **kwargs)
            exception = False
        except Exception as error:
            exception = True
            string = ''.join(traceback.format_exception(None, error, error.__traceback__))
            out = string, error
        finally:
            if not exception:
                return out
            else:
                if raise_error:
                    raise out[1]
                else:
                    return out[0]
    return func_exectutor

after decorating the function:

@error_handler(raise_error=False)
def a(x, y):
    try:
        return b(x, y)
    except Exception as e:
        msg = "some unknown error occured"
        raise Exception(msg) from e   

The output of the function if fails, is a string that contains the traceback of all the possible errors that occurred. If not the output is the expected one

a(4, 10)
>>> 14

a('test', 10)
>>> 'Traceback (most recent call last):\n  File "/tmp/ipykernel_60630/2959315277.py", line 3, in c\n    return int(x), int(y)\nValueError: invalid literal for int() with base 10: \'test\'\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n  File "/tmp/ipykernel_60630/2959315277.py", line 10, in b\n    x, y = c(x, y)\n  File "/tmp/ipykernel_60630/2959315277.py", line 6, in c\n    raise Exception(msg) from e\nException: x or y is probably not a number\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n  File "/tmp/ipykernel_60630/2959315277.py", line 19, in a\n    return b(x, y)\n  File "/tmp/ipykernel_60630/2959315277.py", line 14, in b\n    raise Exception(msg) from e\nException: issue during sum of x and y\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n  File "/tmp/ipykernel_60630/879165569.py", line 13, in func_exectutor\n    out = func(*args, **kwargs)\n  File "/tmp/ipykernel_60630/2959315277.py", line 22, in a\n    raise Exception(msg) from e\nException: some unknown error occured\n'
Related