n8n-io/n8n · error · TaskSubprocessFailedError
Task subprocess exited with code {exit_code}
Error message
Task subprocess exited with code {exit_code} What it means
TaskSubprocessFailedError is raised when the child process exited with a non-zero code that is neither SIGTERM nor SIGKILL. The asserted exit_code is propagated so the caller can see exactly how the child died. It is the catch-all for 'the Python task itself crashed' as opposed to being cancelled, killed, or timing out.
Source
Thrown at packages/@n8n/task-runner-python/src/task_executor.py:241
raise TaskSubprocessFailedError(-1, e)
finally:
write_conn.close()
process.join(timeout=task_timeout)
if process.is_alive():
TaskExecutor.stop_process(process)
raise TaskTimeoutError(task_timeout)
if process.exitcode == SIGTERM_EXIT_CODE:
raise TaskCancelledError()
if process.exitcode == SIGKILL_EXIT_CODE:
raise TaskKilledError()
if process.exitcode != 0:
assert process.exitcode is not None
raise TaskSubprocessFailedError(process.exitcode)
pipe_reader.join(timeout=task_timeout)
if pipe_reader.is_alive():
try:
read_conn.close()
except Exception:
pass
raise TaskResultReadError(
TimeoutError(f"Pipe reader timed out after {task_timeout}s")
)
if pipe_reader.error:
raise TaskResultReadError(pipe_reader.error)
if pipe_reader.pipe_message is None:
raise TaskResultMissingError()
View on GitHub (pinned to 5ac6606e81)
Solutions
- Wrap the body of user code in try/except and either return a structured error via the result pipe or re-raise with context, so the failure surfaces as a TaskRuntimeError (error key) rather than a non-zero exit.
- Reproduce the snippet locally with the same Python and dependency versions to find the unhandled exception; the child's stderr usually has the traceback.
- If exit_code is -1 from the spawn path, check that the fork server / Python interpreter / venv used by the runner is healthy and importable.
- For native crashes (segfault), align the native library versions (numpy/scipy/openssl) between the runner image and the user's expectations.
Example fix
// before
result = do_work() # raises KeyError, child exits non-zero
// after
try:
result = do_work()
except Exception as e:
result = {'error': str(e)}
Defensive patterns
Strategy: try-catch
Validate before calling
# Validate the snippet parses before submitting it.
import ast
try:
ast.parse(user_code)
except SyntaxError as e:
raise ValueError(f"refusing to run unparseable task: {e}")
Try / catch
from n8n_task_runner.errors import TaskSubprocessFailedError
try:
result = TaskExecutor.execute_process(proc, rc, wc, task_timeout, continue_on_fail=False)
except TaskSubprocessFailedError as e:
# e.args[0] is the exit code; capture child stderr separately for the traceback
return [{"json": {"error": f"subprocess exit {e.args[0]}", "detail": str(e)}}], [], 0
Prevention
- Validate that user code parses (ast.parse) before launching the subprocess.
- Have user code catch its own exceptions and return structured errors.
- Pin native dependency versions to avoid interpreter crashes.
- Capture and log child stderr so non-zero exits are diagnosable.
When it happens
Trigger: User Python code raised an unhandled exception that bubbled to the top level of the child, the child called sys.exit(non_zero), or a C extension aborted the interpreter (segfault manifests as a negative signal-style exit code). The inner try/except around process.start() also rewraps spawn-time failures as TaskSubprocessFailedError(-1, e).
Common situations: An exception (NameError, KeyError, ZeroDivisionError, etc.) escapes the user's snippet; sys.exit(1) is called explicitly; a native dependency (e.g. numpy/openssl mismatch) crashes the interpreter; the fork server itself failed to spawn the worker.
Related errors
- Task subprocess exited with code -1
- Task execution timed out after {task_timeout} seconds
- Task was cancelled
- Process was forcefully killed (SIGKILL)
- Security violation detected
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/cd8895a2d7b10b7f.
Report an issue: GitHub.