Custom exceptions – catching classes that do not inherit from BaseException

Question:

I’m trying to write some custom exception handling but keep running into issues with ‘TypeError: catching classes that do not inherit from BaseException is not allowed’ errors. I have a base exception class called NodeError that inherits from Exception. From there, I have several custom exceptions that inherit from NodeError.

The web3 module uses the requests module to communicate with a node. My test constantly tries to get the tx count from the node and while it does that, I try to simulate an outage by disabling my NIC. I try to catch requests.exceptions.ConnectionError in get_tx_count() and raise my own exception. It seems to correctly hit the NodeConnectionError custom exception based on the stack trace but then gets another exception and complains about catching classes that don’t inherit from BaseException.

Not sure why it thinks I’m not inheriting from BaseException but I have a feeling it has to do with catching the requests exception first.

Stack trace:

Traceback (most recent call last):
  File "C:Python39libsite-packagesurllib3connection.py", line 169, in _new_conn
    conn = connection.create_connection(
  File "C:Python39libsite-packagesurllib3utilconnection.py", line 73, in create_connection
    for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
  File "C:Python39libsocket.py", line 953, in getaddrinfo
    for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
socket.gaierror: [Errno 11001] getaddrinfo failed

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:Python39libsite-packagesurllib3connectionpool.py", line 699, in urlopen
    httplib_response = self._make_request(
  File "C:Python39libsite-packagesurllib3connectionpool.py", line 382, in _make_request
    self._validate_conn(conn)
  File "C:Python39libsite-packagesurllib3connectionpool.py", line 1010, in _validate_conn
    conn.connect()
  File "C:Python39libsite-packagesurllib3connection.py", line 353, in connect
    conn = self._new_conn()
  File "C:Python39libsite-packagesurllib3connection.py", line 181, in _new_conn
    raise NewConnectionError(
urllib3.exceptions.NewConnectionError: <urllib3.connection.HTTPSConnection object at 0x000001413971AB50>: Failed to establish a new connection: [Errno 11001] getaddrinfo failed

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:Python39libsite-packagesrequestsadapters.py", line 439, in send
    resp = conn.urlopen(
  File "C:Python39libsite-packagesurllib3connectionpool.py", line 755, in urlopen
    retries = retries.increment(
  File "C:Python39libsite-packagesurllib3utilretry.py", line 574, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host='matic-mainnet-full-rpc.bwarelabs.com', port=443): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x000001413971AB50>: Failed to establish a new connection: [Errno 11001] getaddrinfo failed'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:textest.py", line 87, in get_tx_count
    nonce = w3.eth.get_transaction_count(address)
  File "C:Python39libsite-packagesweb3module.py", line 57, in caller
    result = w3.manager.request_blocking(method_str,
  File "C:Python39libsite-packagesweb3manager.py", line 186, in request_blocking
    response = self._make_request(method, params)
  File "C:Python39libsite-packagesweb3manager.py", line 147, in _make_request
    return request_func(method, params)
  File "cytoolz/functoolz.pyx", line 250, in cytoolz.functoolz.curry.__call__
    return self.func(*args, **kwargs)
  File "C:Python39libsite-packagesweb3middlewareformatting.py", line 76, in apply_formatters
    response = make_request(method, params)
  File "C:Python39libsite-packagesweb3middlewaregas_price_strategy.py", line 84, in middleware
    return make_request(method, params)
  File "cytoolz/functoolz.pyx", line 250, in cytoolz.functoolz.curry.__call__
    return self.func(*args, **kwargs)
  File "C:Python39libsite-packagesweb3middlewareformatting.py", line 74, in apply_formatters
    response = make_request(method, formatted_params)
  File "C:Python39libsite-packagesweb3middlewareattrdict.py", line 33, in middleware
    response = make_request(method, params)
  File "cytoolz/functoolz.pyx", line 250, in cytoolz.functoolz.curry.__call__
    return self.func(*args, **kwargs)
  File "C:Python39libsite-packagesweb3middlewareformatting.py", line 74, in apply_formatters
    response = make_request(method, formatted_params)
  File "cytoolz/functoolz.pyx", line 250, in cytoolz.functoolz.curry.__call__
    return self.func(*args, **kwargs)
  File "C:Python39libsite-packagesweb3middlewareformatting.py", line 76, in apply_formatters
    response = make_request(method, params)
  File "cytoolz/functoolz.pyx", line 250, in cytoolz.functoolz.curry.__call__
    return self.func(*args, **kwargs)
  File "C:Python39libsite-packagesweb3middlewareformatting.py", line 74, in apply_formatters
    response = make_request(method, formatted_params)
  File "C:Python39libsite-packagesweb3middlewarebuffered_gas_estimate.py", line 40, in middleware
    return make_request(method, params)
  File "C:Python39libsite-packagesweb3middlewareexception_retry_request.py", line 105, in middleware
    return make_request(method, params)
  File "C:Python39libsite-packagesweb3providersrpc.py", line 88, in make_request
    raw_response = make_post_request(
  File "C:Python39libsite-packagesweb3_utilsrequest.py", line 48, in make_post_request
    response = session.post(endpoint_uri, data=data, *args, **kwargs)  # type: ignore
  File "C:Python39libsite-packagesrequestssessions.py", line 590, in post
    return self.request('POST', url, data=data, json=json, **kwargs)
  File "C:Python39libsite-packagesrequestssessions.py", line 542, in request
    resp = self.send(prep, **send_kwargs)
  File "C:Python39libsite-packagesrequestssessions.py", line 655, in send
    r = adapter.send(request, **kwargs)
  File "C:Python39libsite-packagesrequestsadapters.py", line 516, in send
    raise ConnectionError(e, request=request)
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='matic-mainnet-full-rpc.bwarelabs.com', port=443): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x000001413971AB50>: Failed to establish a new connection: [Errno 11001] getaddrinfo failed'))

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:textest.py", line 114, in main
    print(get_tx_count(w3, address))
  File "C:textest.py", line 90, in get_tx_count
    raise NodeConnectionError(w3) from e
__main__.NodeConnectionError: An error occurred with the node at A web3 connection error occurred talking to https://matic-mainnet-full-rpc.bwarelabs.com:443..

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:textest.py", line 140, in <module>
    main()
  File "C:textest.py", line 116, in main
    except NodeNotConnected(w3.provider.endpoint_uri, w3):
TypeError: catching classes that do not inherit from BaseException is not allowed

Test code:

from web3 import Web3
from time import sleep
from requests.exceptions import ConnectionError, ConnectTimeout, HTTPError
import sys

class NodeError(Exception):
    """Base exception for node errors"""
    def __init__(self, url, msg=None):
        if msg is None:
            msg = f"An error occurred with the node at {url}."
        super().__init__(msg)
        self.url = url

class NodeNotConnected(NodeError):
    """web3 could not connect to a node"""
    def __init__(self, url, w3=None):
        msg = f"A web3 connection could not be made to URL {url}."
        super().__init__(url, msg=msg)
        self.w3 = w3

class NodeConnectionError(NodeError):
    """A web3 error occurred communicating with a node"""
    def __init__(self, w3):
        msg = (
            f"A web3 connection error occurred talking to "
            f"{w3.provider.endpoint_uri}."
        )
        super().__init__(msg)
        self.w3 = w3

class NodeTooManyRequests(NodeError):
    def __init__(self, w3):
        msg = (
            f"Too many requests made to {w3.provider.endpoint_uri}.  Try a "
            f"different node."
        )
        super().__init__(msg)
        self.w3 = w3

class NodeNoAvailableNodes(NodeError):
    def __init__(self):
        msg = f"Unable to connect to any nodes."
        super().__init__(msg)

class NodeInternalError(NodeError):
    def __init__(self):
        msg = "The node had an internal error."
        super().__init__(msg)

def lib_connect_to_node(url):
    """Emulates library connect to node function"""
    try:
        w3 = Web3(
            Web3.HTTPProvider(
                url,
                request_kwargs={"timeout": 5}
            )
        )
        if not w3.isConnected():
            raise NodeNotConnected(url, w3)
    except Exception as e:
        raise NodeNotConnected(url) from e
    else:
        return w3

def connect_to_node(urls, node_retries):
    while node_retries >= 0:
        try:
            w3 = lib_connect_to_node(urls[0])
        except NodeNotConnected(urls[0]) as e:
            if node_retries == 0:
                raise NodeNoAvailableNodes from e
            node_retries -= 1
            urls = get_next_node(urls)
            print('Trying another node')
            sleep(1)
            continue
        else:
            return w3

def get_next_node(urls):
    urls.append(urls.pop(urls.index(urls[0])))
    return urls

def get_tx_count(w3, address):
    try:
        nonce = w3.eth.get_transaction_count(address)
    except (ConnectionError, ConnectTimeout) as e:
        print("A requests.exceptions.ConnectionError occurred.")
        raise NodeConnectionError(w3) from e
    except HTTPError as e:
        if e.code == 429:
            raise NodeTooManyRequests from e
    else:
        return nonce

def main():
    urls = [
        'https://matic-mainnet-full-rpc.bwarelabs.com:443',
        'https://matic-mainnet.chainstacklabs.com:443',
        'https://rpc-mainnet.maticvigil.com:443',
        'https://rpc-mainnet.matic.network:443'
    ]
    node_retries = 3
    address = '0xe18A0D121057B002BaFb90aD5F1AB951594A61E8'
    try:
        w3 = connect_to_node(urls, node_retries)
    except NodeNoAvailableNodes as e:
        print(e)
        sys.exit()

    while True:
        try:
            print(get_tx_count(w3, address))
            sleep(0.05)
        except NodeNotConnected(w3.provider.endpoint_uri, w3):
            urls = get_next_node(urls)
            try:
                w3 = connect_to_node(urls, node_retries)
            except NodeNoAvailableNodes as e:
                print(e)
                sys.exit()
        except NodeConnectionError(w3) as e:
            urls = get_next_node(urls)
            try:
                w3 = connect_to_node(urls, node_retries)
            except NodeNoAvailableNodes as e:
                print(e)
                sys.exit() 
        except NodeTooManyRequests(w3) as e:
            print('Too many requests')
            urls = get_next_node(urls)
            try:
                w3 = connect_to_node(urls, node_retries)
            except NodeNoAvailableNodes as e:
                print(e)
                sys.exit()

if __name__ == '__main__':
    main()
Asked By: Netman

||

Answers:

You should be able to catch anything that is an exception, even if it doesn’t directly inherit from BaseException.

However, remove the class parameter from the except clause, and have only the class name:

try:
    raise NodeNotConnected('abc')
except NodeNotConnected as e:
    print(e)

Result:

A web3 connection could not be made to URL abc.

When you specify the class parameters, a class is instantiated, which evaluates to an instance instead of a type.

Answered By: ELinda