Python threading exception handling (tested)
An exception inside a Python thread doesn't propagate to the code that started it. We measured what actually happens on Python 3.13.5 and the three ways to get the error back: threading.excepthook, future.result() and map().
1. An exception in a Thread doesn't reach the main thread
import threading
def work():
raise ValueError("bad row 42")
t = threading.Thread(target=work)
try:
t.start()
t.join()
print("join() returned normally; the main thread never saw the error")
except ValueError:
print("caught in main")
Output:
Exception in thread Thread-1 (work):
Traceback (most recent call last):
File "/usr/lib/python3.13/threading.py", line 1043, in _bootstrap_inner
self.run()
~~~~~~~~^^
File "/usr/lib/python3.13/threading.py", line 994, in run
self._target(*self._args, **self._kwargs)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/th/b1.py", line 3, in work
raise ValueError("bad row 42")
ValueError: bad row 42
join() returned normally; the main thread never saw the error
Measured on Python 3.13.5: the thread prints its traceback to stderr and dies, but join() returns normally and the except around it never runs. The script then exited with status 0, so a cron job or CI step would report success.
2. threading.excepthook: collect errors from every thread
import threading
errors = []
def hook(args):
errors.append((args.thread.name, args.exc_type.__name__, str(args.exc_value)))
threading.excepthook = hook
def work(n):
if n == 2:
raise ValueError(f"bad row {n}")
threads = [threading.Thread(target=work, args=(n,), name=f"w{n}") for n in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print("errors collected:", errors)
Output:
errors collected: [('w2', 'ValueError', 'bad row 2')]
Measured: threading.excepthook (Python 3.8+) is called with the thread, exception type and value for every uncaught exception in a thread. Replacing it lets you log the errors or record them and fail the program at the end. It replaces the default traceback printing, so log the traceback yourself if you want it.
3. ThreadPoolExecutor: result() re-raises, but only if you call it
from concurrent.futures import ThreadPoolExecutor
def work(n):
if n == 2:
raise ValueError(f"bad row {n}")
return n * 10
with ThreadPoolExecutor(4) as pool:
futures = [pool.submit(work, n) for n in range(4)]
for n, f in enumerate(futures):
try:
print(n, "->", f.result()) # re-raises the worker's exception here
except ValueError as e:
print(n, "-> ValueError:", e)
with ThreadPoolExecutor(4) as pool:
pool.submit(work, 2) # result() never called
print("an unchecked future's exception is silently dropped")
Output:
0 -> 0
1 -> 10
2 -> ValueError: bad row 2
3 -> 30
an unchecked future's exception is silently dropped
Measured: with concurrent.futures, the worker's exception is stored in the future and re-raised by f.result(), so you can catch it in the calling thread like a normal exception. If nobody calls result(), the exception is dropped without any output at all, which is worse than plain threads.
4. pool.map() stops at the first error
from concurrent.futures import ThreadPoolExecutor
def work(n):
if n == 2:
raise ValueError(f"bad row {n}")
return n * 10
with ThreadPoolExecutor(4) as pool:
try:
print(list(pool.map(work, range(4))))
except ValueError as e:
print("map() raised:", e, "- and the results for 0 and 1 are lost")
Output:
map() raised: bad row 2 - and the results for 0 and 1 are lost
Measured: map() re-raises the first exception as you iterate, and the successful results collected so far are lost with it. If you need every result plus every error, use submit() and check each future as in section 3. For handling Ctrl+C with threads, see our KeyboardInterrupt guide.
Frequently asked questions
How do I catch an exception from a thread in Python?
You can't with try/except around join(): we measured join() returning normally after the thread raised. Use threading.excepthook, or ThreadPoolExecutor and future.result(), which re-raises it.
Why does my script exit with code 0 when a thread crashed?
An uncaught exception in a thread only prints a traceback. We measured exit status 0 after a thread raised ValueError on Python 3.13.5.
What is threading.excepthook?
A function Python calls for every uncaught exception in a thread, with the thread, type and value. We measured a custom hook collecting the error from one of four threads.
Does ThreadPoolExecutor swallow exceptions?
Only if you never call result(). We measured result() re-raising the worker's ValueError, and an unchecked future's exception producing no output at all.
Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5: a raising Thread with join() and its exit status, a custom threading.excepthook, ThreadPoolExecutor submit/result with and without result(), and pool.map with one failing item. Every block was run verbatim and its output pasted from that run. PlainHeaders is our extension; everything else here is built-in Python.