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

  1. 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.
  2. Reproduce the snippet locally with the same Python and dependency versions to find the unhandled exception; the child's stderr usually has the traceback.
  3. 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.
  4. 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

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


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/cd8895a2d7b10b7f. Report an issue: GitHub.