Search code examples
pythonpython-3.xmultithreadingerror-handlingpython-multithreading

How are errors handled within threads in Python?


I am using ThreadPoolExecutor to execute a function which causes an error. When an error is caused within a thread (and its not handled), it looks like it is terminating that thread, but not printing anything to stderr or causing the program to crash. Why does this happen? Can I make it so that an unhandled error in any thread causes the whole program to crash? Alternatively it could print somewhere so I can see what happened.

I've noticed that the error can be caught in a try-catch block, so it is definitely being raised. If the same function is called without threading then the error is raised as expected.

from concurrent.futures import ThreadPoolExecutor
import time


def error_causer(x):
    print(x)
    raise ValueError("This bad!")


with ThreadPoolExecutor(max_workers=1) as executor:
    executor.map(error_causer, ["coming from thread"])

print("After don't catch. Should have crashed by now...")
time.sleep(2)

error_causer("coming from outside thread")

This program doesn't crash when the ValueError is raised within the thread, but does crash when the ValueError is raised outside the thread.

The output is:

coming from thread
After don't catch. Should have crashed by now...
coming from outside thread
Traceback (most recent call last):
  File "./another_threading_test.py", line 16, in <module>
    error_causer("coming from outside thread")
  File "./another_threading_test.py", line 7, in error_causer
    raise ValueError("This bad!")
ValueError: This bad!

I would expect it to crash before the print("after don't catch")


Solution

  • The executor.map() will not raise the error as the worker thread is terminated silently without raising the error unless you try to retrieve the result from the iterator this is as per the docs:

    map(func, *iterables, timeout=None, chunksize=1)

    If a func call raises an exception, then that exception will be raised when its value is retrieved from the iterator.

    You can verify this by invoking next() on the returned iterator:

    def error_causer(x):
        print(x)
        raise ValueError("This bad!")
    
    
    with ThreadPoolExecutor(max_workers=1) as executor:
        iterator = executor.map(error_causer, ["coming from thread"])
        next(iterator)
    

    Now after you call the next() on the iterator you would see the error being raised in the main thread coming from worker thread which terminates the main thread:

    Traceback (most recent call last):
      File "/Users/ambhowmi/PycharmProjects/Datastructures/TwoStacksQueue.py", line 13, in <module>
        next(iterator)
      File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/concurrent/futures/_base.py", line 598, in result_iterator
        yield fs.pop().result()
      File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/concurrent/futures/_base.py", line 428, in result
        return self.__get_result()
      File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/concurrent/futures/_base.py", line 384, in __get_result
        raise self._exception
      File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/concurrent/futures/thread.py", line 57, in run
        result = self.fn(*self.args, **self.kwargs)
      File "/Users/ambhowmi/PycharmProjects/Datastructures/TwoStacksQueue.py", line 8, in error_causer
        raise ValueError("This bad!")
    ValueError: This bad!