Can a parent process catch an exception raised inside a child process?
answer
- Separate processes, separate stacks
- Nothing unwinds across the boundary
- The object must be serialized first
- join() returns, it never raises
- exitcode: zero, positive, or negative
basics
~20 sNo. A child is a separate OS process with its own memory and its own call stack, so the parent's try/except never wraps it. The exception object must be serialized and re-raised in the parent, or the parent learns only from the child's exit status.
solid answer
~40 sA child process runs its own interpreter with its own stack, so nothing propagates upward the way it would inside one process. With a bare `multiprocessing.Process`, an uncaught exception prints the child's traceback to the child's stderr and the child exits with status 1; the parent's `join()` returns normally and raises nothing, and `Process.exitcode` is the only in-band signal it gets. Machinery that hands you a result object instead pickles the exception in the child, ships the bytes, and re-raises a reconstructed copy where you ask for the result. And some failures never produce an exception at all: a child killed by a signal, or one that dies during startup import, leaves no object to serialize, so the exit status is all you have.
code
python · 15 linesimport multiprocessing as mp
def child():
raise ValueError("target list empty")
if __name__ == "__main__":
p = mp.Process(target=child)
try:
p.start()
p.join()
except ValueError:
print("parent caught it") # never runs
print("exitcode:", p.exitcode) # 1go deeper
Recall the one-line reason: separate processes have separate memory and separate stacks, so an exception cannot unwind into the parent. Know that you check the child's exit status instead.
Explain the two paths precisely: a bare child prints its traceback and exits nonzero, while result-carrying machinery pickles the exception and re-raises a copy. Be able to read Process.exitcode values, including negative ones.
Show that you design for it: children that handle their own failures, exit codes that mean something, child stderr captured, and no code that treats a returning join() as success. Mention import-time deaths under spawn and forkserver.
Own the reporting contract across the fleet: what a worker failure looks like in logs and metrics, whether failures must be structured data rather than re-raised objects, and how the 3.14 start-method default change alters failure modes in existing services.
**Two processes means two of everything.** Exception propagation in Python is a property of one interpreter's call stack: `raise` unwinds frames looking for a handler, and if none is found the interpreter prints the traceback and exits. A child process has its own frames, its own `sys.modules`, its own heap. There is no shared stack for a `raise` to unwind into, so the parent's `try`/`except` cannot possibly be the handler for something raised in the child. This is the single fact everything else on this subject follows from. **What a bare child gives you.** With `multiprocessing.Process`, the child runs your target function inside a bootstrap wrapper. If the function raises, that wrapper prints the traceback to the child's own standard error and terminates the child with exit status 1. In the parent, `start()` returns immediately and `join()` returns when the child is gone -- neither raises. The only structured information that crosses is `Process.exitcode`: * `None` -- the process has not finished yet (or was never started). Reading `exitcode` before `join()` and treating `None` as success is a classic bug. * `0` -- the child ran to completion. * a positive number -- the child ended deliberately: an uncaught exception gives 1, and `sys.exit(n)` or `os._exit(n)` gives `n`. * a negative number `-N` -- the child was killed by signal `N`. `-9` is `signal.SIGKILL`, which is what an out-of-memory killer or an impatient supervisor sends. No Python-level exception ever existed in that case; the process was stopped mid-instruction. **What machinery with results gives you.** Anything that returns a future, an async result or a mapped sequence takes a different route: it catches the exception inside the child, pickles it, sends the bytes back over a pipe, and re-raises the reconstructed copy in the parent at the point where you ask for the value. That is a genuinely useful illusion, and it is why the rest of this subject exists -- the copy is only as faithful as pickle allows. The class has to be importable in the parent by module path and qualified name, the constructor arguments have to round-trip, and the traceback does not travel at all, because traceback objects hold live frames and cannot be pickled. **Startup failures are the nastiest version.** Since 3.14 the default start method is `forkserver` on Unix other than macOS, while macOS and Windows use `spawn`; `fork` must now be requested explicitly. Both `spawn` and `forkserver` start a fresh interpreter that re-imports your module rather than inheriting the parent's memory. So an import-time problem the parent happened to get past -- a cycle between modules that only resolves in one import order, a missing optional dependency, a module-level connection attempt -- can blow up inside the child during bootstrap, before your target function is ever called. Nothing you wrote runs, nothing is pickled, and the parent sees a nonzero exit status with a traceback on the child's stderr that is easy to lose if you redirect or discard it. Imagine a metrics scraper that fans out one worker per scrape target across a 17-service dependency graph: under `fork` the workers inherited a fully imported parent and started fine, and after the 3.14 default change every worker re-imports and dies on the import cycle, with the parent reporting nothing worse than "a worker exited". **How to work with the grain.** Give the child a handler of its own: wrap the target body in `try`/`except`, log or format the failure there, and exit with a status that means something. Never assume `join()` returning means success -- always check `exitcode`. Keep the child's stderr somewhere you can read it. And when you need real detail back in the parent, plan for the serialization boundary explicitly rather than hoping the exception arrives intact. **Where the child's traceback actually goes.** It is written to the child's own standard error by the interpreter, exactly as it would be for a script run from a terminal. If the child inherited a terminal you see it interleaved with the parent's output and it looks as though the parent reported it -- it did not. If the child's stderr was redirected to a pipe nobody drains, or to a null device, the text is simply gone and the exit status is all that remains. So "the parent cannot catch it" has a practical corollary: whoever starts children owns the question of where their stderr lands, and a worker that logs its own failures before dying is worth far more than one that relies on the default.
- What does a `multiprocessing.Process.exitcode` of -9 tell you?The child was killed by signal 9, `signal.SIGKILL`. A negative exit code means "terminated by signal N", so no Python exception was ever raised and no handler ran -- the process stopped mid-instruction. In practice that is an out-of-memory killer or an external supervisor. Positive codes are deliberate exits: 1 for an uncaught exception, otherwise the value passed to `sys.exit` or `os._exit`. `None` means the child has not finished.
- Why can a child fail under `spawn` when the same code worked under `fork`?`fork` clones the parent's already-initialised memory, so the child skips importing entirely. `spawn` and `forkserver` start a fresh interpreter that re-imports your module, which re-runs every module-level statement in the child. Import cycles, module-level side effects and anything that only worked because the parent imported things in a particular order now surface in the child. The target callable and its arguments must also be picklable, which `fork` never required.
- How should a child report failure when there is no result object to carry it?Handle it inside the child: wrap the target body in `try`/`except`, log or write the formatted traceback where the parent can read it, and exit with a status that encodes the outcome via `sys.exit(n)`. Then the parent checks `Process.exitcode` after `join()` and maps codes to meanings. Keep the child's stderr captured rather than discarded, since the interpreter writes uncaught tracebacks there.
Shouting in one room does not interrupt a conversation in another. You either write the message down and slide it under the door, or the only thing the other room notices is that you stopped answering.
saying these in an interview costs you the question
- Claims the parent's try/except wraps the child's code
- Thinks join() re-raises whatever the child raised
- Treats a child process like a thread sharing a stack
- Reads an exitcode of None as a clean exit
- Assumes every child failure produces an exception object
- Believes the child's traceback appears in the parent automatically