Skip to content

[Bug] CustomThread silently discards exceptions from background solver threads — callers receive None on failure #45

@tejassinghbhati

Description

@tejassinghbhati

Summary

Summary

CustomThread extends threading.Thread to capture and return the result of a background thread's target function via a custom join(). However, if the target raises an exception, the exception is silently swallowed — self._return stays None and the exception is lost forever. The calling code receives None as if the thread completed successfully, making it impossible to distinguish a clean None return from a thread crash.

This is especially dangerous for model solver threads, where a silent failure gives users false confidence that a run completed — while results are never actually produced.


Affected File

API/Classes/Base/CustomThreadClass.py


Current Behaviour

class CustomThread(Thread):
    def run(self):
        if self._target is not None:
            # ❌ If _target raises, exception is silently swallowed here
            self._return = self._target(*self._args, **self._kwargs)

    def join(self):
        Thread.join(self)
        # ❌ Returns None whether thread succeeded or crashed
        return self._return

If the solver subprocess raises a FileNotFoundError, RuntimeError, or any other exception:

  • self._return is never assigned → stays None
  • The exception is lost — it does not propagate to the main thread
  • join() returns None to the caller, which looks identical to a valid empty result
  • No error is logged, no user feedback is shown

Expected behavior

Expected Behaviour

  • If a background thread raises an exception, that exception must propagate to the caller when join() is called
  • join() should re-raise the exception so the route handler can catch it and return a proper error response to the frontend
  • No silent failures — the exception must never be discarded

Proposed Fix

import sys

class CustomThread(Thread):
    def __init__(self, group=None, target=None, name=None, args=(), kwargs={}, Verbose=None):
        Thread.__init__(self, group, target, name, args, kwargs)
        self._return = None
        self._exc_info = None  # stores exception if thread crashes

    def run(self):
        if self._target is not None:
            try:
                self._return = self._target(*self._args, **self._kwargs)
            except Exception:
                self._exc_info = sys.exc_info()  # capture full traceback

    def join(self):
        Thread.join(self)
        if self._exc_info is not None:
            # re-raise the original exception with its original traceback
            raise self._exc_info[1].with_traceback(self._exc_info[2])
        return self._return

Reproduction steps

Steps to Reproduce

from API.Classes.Base.CustomThreadClass import CustomThread

def failing_task():
    raise RuntimeError("Solver failed: file not found")

t = CustomThread(target=failing_task)
t.start()
result = t.join()

# Bug: result is None, no exception is raised
# Expected: RuntimeError should propagate here

print(result)  # prints None — silent failure

Environment

Environment

Field Value
Affected file API/Classes/Base/CustomThreadClass.py
Python 3.10+
Impact All background solver runs using CustomThread

Acceptance Criteria

  • Exceptions raised inside a CustomThread target propagate to the caller on join()
  • The original traceback is preserved (not replaced with a new one)
  • join() still returns the target's return value on success
  • A unit test is added: one for successful return, one for exception propagation

Labels

bug · correctness · threading · GSoC-2026

Logs or screenshots

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions