Skip to main content

A simple framework for customized error handling

Project description

Error Handler

What is it?

This module implements a simple, yet flexible framework for handling errors and applying specific strategies for various cases. Basically, a function is decorated with the error-handler wrapper, with or without a specific strategy, and exceptions (and for given strategies other error situations) can be caught, processed and handled according to the needs of the user.

Basic usage

The simplest use will be along these lines:

@error_handler
def error_prone_func(s: str):
    ...
    < code that might fail >
    ...
    return result

This will add a basic error handler to the function, using a default error strategy (just basic logging and re-raising the exception). In this case, the handler will catch any exception from the function, handle it according to the default strategy, and finally raise the exception after processing. Functionally, the benefits might be small, but it gives cleaner code with less nesting.

To supress the exception instead (not re-raised), this can be done by simply setting the re_raise property of the handler:

@error_handler(re_raise=False)
def function(x: int):
    < code >
    return something

This will still handle the exception using the supplied strategy, but it will not be raised further allowing normal execution to proceed (typically if the handled function is not really required, or if an error is not relevant to the process)

A slightly more involved example using retry:

@error_handler(strategy=RetryStrategy())
def my_function(x: int, y: int) -> int:
    < code >
    return something

This will use the RetryStrategy, providing basic retry functionality for the function. Note that the function as a whole will be retried, with the same arguments. If all retries fail, the strategy will return a result indicating failure, and the exception will be raised back to the caller.

Strategy configuration

The strategies may also be configured more specifically to each case, like this:

@error_handler(strategy=RetryStrategy(retries=5, backoff=3, backoff_exponent=2, on_except_hook=do_on_exception_func))
def my_function(x: int, y: int) -> int:
    < code >
    return something

This will override the default values of the strategy, using the user supplied values instead and in this case adding a function to be called on each exception when attempting the function/retry. To reduce clutter and simplify reuse these configurations can be instantiated outside the decorator and used where needed:

# Set up different strategies for use in code

# A simple retry strategy
simple_retry = RetryStrategy(retries=5)

# Retry with exponential backoff
exp_backoff_retry = RetryStrategy(retries=10, backoff_exponent=2)

# More complex handling of retries
complex_retry = RetryStrategy(
    retries=4,
    backoff=5,
    backoff_exponent=2,
    handled_exceptions=(HTTPError, IOError, ConnectionError),
    on_except_hook=my_custom_error_logger,
    on_except_hook_args={"foo": "bar", "reference": transaction_id}
)

# -- End of strategies

@error_handler(strategy=simple_retry)
def func_a(n: int):
    <code>
    return something

@error_handler(strategy=exp_backoff_retry)
def func_b(n: int):
    <code>
    return something

@error_handler(strategy=simple_retry)
def func_c(n: int):
    <code>
    return something

@error_handler(strategy=complex_retry)
def func_d(n: int):
    <code>
    return something

Advanced usage

It is also possible to use strategies for handling other types of errors than exceptions. A typical case here would be to handle the response of an http-request based on status code. This is made trickier by the fact that you probably need to handle the result of the actual request, rather than the function containing the calling code.

As an example, we have the following function:

def call_service():
    <prepare request>
    response = requests.post(url, headers=headers, data=request_data)
    <process response>
    return result

In this case, decorating the function with an error_handler wouldn't catch an error from the requests.post() call, since the request would return a status code on the response object indicating the result, whether it is a success or otherwise (unless the call itself generated an exception i.e. unable to serialize the data element) so the previous method of simply decorating like this wouldn't give us what we wanted:

@error_handler(strategy=HttpErrorStrategy())
def call_service():
    ...

It would catch exceptions from trying to process a missing body after an internal server error, but that might not be what we want. Let's say you know the service you are trying to call has availability issues, but getting the call across is critical to your application you might need to retry the call several times before allowing the program to try to process the result of the request. Simply setting the retry_on_error=True for the HttpErrorStrategy doesn't help, since we need to verify (handle) the response from the requests.post() directly. This can be achieved through calling the decorator directly with the wrapped function call:

response = error_handler(strategy=HttpErrorStrategy(retry_on_error=True))(requests.post)(url, headers=headers, data=request_data)

That looks complicated, but the structure is actually quite straight forward:

decorator(<config parameters>)(<function to wrap>)(<function arguments>)

This syntax allows for using a handler directly on a single line of code if required. Since decorators can be nested, this allows for more complex handling, like in this imaginary example:

# Set up default handling with additional custom logging on exception
@error_handler(strategy=DefaultHandlingStrategy(on_except_hook=my_custom_logger))
def example():
    <code>

    # Add retry for critical code
    @error_handler(strategy=RetryStrategy())
    def inner_function():
        <code>
        result = call_external_service()
        <code>
        # supress exceptions from non-critical component
        @error_handler(re_raise=False)(call_non_required_func)("data":result.content)
        return result
    
    <code>
    temp_result = inner_function()
    <process temp_result>
    return temp_result

This sets up a function with default error handling and custom logging, while enabling retry for an inner function (not the whole function). There is also a call to an unreliable dependency that can't be allowed to let the execution fail (maybe has a tendency to cause timeouts due to long processing, but still get's the job done), so that call is wrapped in an explicit local handler supressing any exceptions raised.

Writing your own strategies

It is also pretty straight forward to write your own strategies for error handling to plug in:

Define a class that derives from ErrorHandlingStrategy:

class MyErrorStrategy(ErrorHandlingStrategy)

Override the init(self) function:

def __init__(
        self,
        *,
        handled_exceptions: Tuple[Type[Exception], ...] = None,
        on_except_hook: Callable[[Exception], None] = None,
        on_except_hook_args: Dict[str, Any] = None,
        # Arguments for custom handler goes here 
    ):
    super().__init__(handled_exceptions=handled_exceptions, on_except_hook=on_except_hook, on_except_hook_args=on_except_hook_args)
    
    # Custom init goes here

Implement the act-function in you strategy. This is called by the error_handler to invoke the wrapped function:

def act(self, func: Callable, *args, **kwargs) -> StrategyInvokeResult:
    # implementation goes here.

Add whatever logic/functionality you need to your strategy.

Once done, it can be injected into a handler easily:

@error_handler(strategy=MyStrategy(<init params>))
def func_to_be_handled():
    # do stuff

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

altinn3_error_handler-0.0.2.tar.gz (10.6 kB view details)

Uploaded Source

Built Distribution

altinn3_error_handler-0.0.2-py3-none-any.whl (11.0 kB view details)

Uploaded Python 3

File details

Details for the file altinn3_error_handler-0.0.2.tar.gz.

File metadata

  • Download URL: altinn3_error_handler-0.0.2.tar.gz
  • Upload date:
  • Size: 10.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.9.16

File hashes

Hashes for altinn3_error_handler-0.0.2.tar.gz
Algorithm Hash digest
SHA256 bd37cd21f36132ef166fe582ae4913e15ad33e52943f534f2887bedfd9e0ff81
MD5 5da31eb5e73b85f0924840fd04304bb0
BLAKE2b-256 d5100b36d4bc01c43bcf718b384372e4c2900e625da63ea7ef32787a80b32375

See more details on using hashes here.

File details

Details for the file altinn3_error_handler-0.0.2-py3-none-any.whl.

File metadata

File hashes

Hashes for altinn3_error_handler-0.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 21ce7e1e55407df6fa751f62373e792c8ebccb916f639a69a1f4e09473c07033
MD5 8b529ee7b9cc48b8f9adb73b6ba8e250
BLAKE2b-256 b91b822a1417a17369dfef603115bb1d15f4469c4bc76797ff6b54e9d3b4a8fa

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page