Skip to content

Commit 3f70ec1

Browse files
committed
fix(core, cli, run-engine): route uncaught exceptions through new INTERNAL_ERROR code
Introduce TASK_RUN_UNCAUGHT_EXCEPTION as a dedicated TaskRunInternalError code so the engine handles retry through its existing crash-style pathway (lockedRetryConfig lookup), and the dashboard renders these failures as "Failed" rather than "System failure". The previous BUILT_IN_ERROR approach showed the right status but didn't respect the user's retry policy: BUILT_IN_ERROR with retry: undefined short-circuits to fail_run because shouldLookupRetrySettings(BUILT_IN_ERROR) returns false. Inline retry calculation in cli-v3 was rejected as duplicating logic already owned by the engine. This change mirrors how TASK_PROCESS_EXITED_WITH_NON_ZERO_CODE, TASK_PROCESS_SIGTERM, and TASK_PROCESS_SIGSEGV already work — same lookup-and-retry pathway, just with a different surface status (Failed vs Crashed) and the original error's message + stackTrace carried on the INTERNAL_ERROR payload. No global behaviour changes; the new code is opt-in via parseExecuteError's UncaughtExceptionError branch. Touchpoints: - packages/core/src/v3/schemas/common.ts: enum entry - packages/core/src/v3/errors.ts: shouldRetryError, shouldLookupRetrySettings - internal-packages/run-engine/src/engine/errors.ts: runStatusFromError - packages/cli-v3/src/executions/taskRunProcess.ts: parseExecuteError + revert TaskRunError widening - tests + changeset + server-changes entry
1 parent 413fa07 commit 3f70ec1

7 files changed

Lines changed: 37 additions & 24 deletions

File tree

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
"trigger.dev": patch
3+
"@trigger.dev/core": patch
34
---
45

5-
Fail attempts on uncaught exceptions instead of hanging to `MAX_DURATION_EXCEEDED`. A Node `EventEmitter` (e.g. `node-redis`) emitting `"error"` with no `.on("error", ...)` listener escalates to `uncaughtException`, which the worker previously reported but did not act on — runs drifted to maxDuration with empty attempts. They now fail fast with the original error and status `FAILED`. You should still attach `.on("error", ...)` listeners to long-lived clients to handle errors gracefully.
6+
Fail attempts on uncaught exceptions instead of hanging to `MAX_DURATION_EXCEEDED`. A Node `EventEmitter` (e.g. `node-redis`) emitting `"error"` with no `.on("error", ...)` listener escalates to `uncaughtException`, which the worker previously reported but did not act on — runs drifted to maxDuration with empty attempts. They now fail fast with the original error and status `FAILED`, and respect the task's normal retry policy. You should still attach `.on("error", ...)` listeners to long-lived clients to handle errors gracefully.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
area: run-engine
3+
type: fix
4+
---
5+
6+
Map the new `TASK_RUN_UNCAUGHT_EXCEPTION` internal-error code to
7+
`COMPLETED_WITH_ERRORS` (Failed) status in `runStatusFromError`. cli-v3
8+
now emits this code when the worker process surfaces an uncaught
9+
exception (e.g. a Node EventEmitter emitting `"error"` with no listener),
10+
so the run renders as a regular task failure in the dashboard rather
11+
than a system failure, while still routing through the engine's
12+
`lockedRetryConfig` lookup so the user's retry policy is honoured.

internal-packages/run-engine/src/engine/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export function runStatusFromError(
1919
case "TASK_INPUT_ERROR":
2020
case "TASK_OUTPUT_ERROR":
2121
case "TASK_MIDDLEWARE_ERROR":
22+
case "TASK_RUN_UNCAUGHT_EXCEPTION":
2223
return "COMPLETED_WITH_ERRORS";
2324
case "TASK_RUN_CANCELLED":
2425
return "CANCELED";

packages/cli-v3/src/executions/taskRunProcess.test.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ describe("TaskRunProcess", () => {
120120
});
121121

122122
describe("parseExecuteError(UncaughtExceptionError)", () => {
123-
it("surfaces the original error as a BUILT_IN_ERROR so the run shows as Failed, not System failure", () => {
123+
it("returns INTERNAL_ERROR with TASK_RUN_UNCAUGHT_EXCEPTION + original message and stack", () => {
124124
const error = new UncaughtExceptionError(
125125
{
126126
name: "Error",
@@ -133,27 +133,23 @@ describe("TaskRunProcess", () => {
133133

134134
const result = TaskRunProcess.parseExecuteError(error);
135135

136-
expect(result.type).toBe("BUILT_IN_ERROR");
137-
if (result.type === "BUILT_IN_ERROR") {
138-
expect(result.name).toBe("Error");
139-
expect(result.message).toBe("read ECONNRESET");
140-
expect(result.stackTrace).toContain("TCP.onStreamRead");
141-
}
136+
expect(result.type).toBe("INTERNAL_ERROR");
137+
expect(result.code).toBe("TASK_RUN_UNCAUGHT_EXCEPTION");
138+
expect(result.message).toBe("read ECONNRESET");
139+
expect(result.stackTrace).toContain("TCP.onStreamRead");
142140
});
143141

144-
it("preserves the original error for unhandledRejection origin too", () => {
142+
it("uses the same code for unhandledRejection origin", () => {
145143
const error = new UncaughtExceptionError(
146144
{ name: "TypeError", message: "boom" },
147145
"unhandledRejection"
148146
);
149147

150148
const result = TaskRunProcess.parseExecuteError(error);
151149

152-
expect(result.type).toBe("BUILT_IN_ERROR");
153-
if (result.type === "BUILT_IN_ERROR") {
154-
expect(result.name).toBe("TypeError");
155-
expect(result.message).toBe("boom");
156-
}
150+
expect(result.type).toBe("INTERNAL_ERROR");
151+
expect(result.code).toBe("TASK_RUN_UNCAUGHT_EXCEPTION");
152+
expect(result.message).toBe("boom");
157153
});
158154
});
159155
});

packages/cli-v3/src/executions/taskRunProcess.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
TaskRunExecution,
99
TaskRunExecutionPayload,
1010
TaskRunExecutionResult,
11-
type TaskRunError,
1211
type TaskRunInternalError,
1312
tryCatch,
1413
WorkerManifest,
@@ -556,7 +555,7 @@ export class TaskRunProcess {
556555
return this._child.connected;
557556
}
558557

559-
static parseExecuteError(error: unknown, dockerMode = true): TaskRunError {
558+
static parseExecuteError(error: unknown, dockerMode = true): TaskRunInternalError {
560559
if (error instanceof CancelledProcessError) {
561560
return {
562561
type: "INTERNAL_ERROR",
@@ -591,16 +590,17 @@ export class TaskRunProcess {
591590
}
592591

593592
if (error instanceof UncaughtExceptionError) {
594-
// Surface the customer's original error as a regular task failure (user
595-
// error → "Failed" status) rather than an internal error → "System
596-
// failure" status. The exception was raised by user code (or a
597-
// dependency the user controls, e.g. an EventEmitter "error" event with
598-
// no listener); it isn't a platform fault.
593+
// Dedicated INTERNAL_ERROR code so the engine handles retry via the
594+
// existing crash-style lookup of run.lockedRetryConfig (same pathway as
595+
// TASK_PROCESS_EXITED_WITH_NON_ZERO_CODE etc.) and so the dashboard
596+
// renders this as "Failed" rather than "System failure" — the exception
597+
// was raised by user code (or a dependency the user controls, e.g. an
598+
// EventEmitter "error" event with no listener), not a platform fault.
599599
return {
600-
type: "BUILT_IN_ERROR",
601-
name: error.originalError.name,
600+
type: "INTERNAL_ERROR",
601+
code: TaskRunErrorCodes.TASK_RUN_UNCAUGHT_EXCEPTION,
602602
message: error.originalError.message,
603-
stackTrace: error.originalError.stack ?? "",
603+
stackTrace: error.originalError.stack,
604604
};
605605
}
606606

packages/core/src/v3/errors.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ export function shouldRetryError(error: TaskRunError): boolean {
395395
case "TASK_EXECUTION_ABORTED":
396396
case "TASK_EXECUTION_FAILED":
397397
case "TASK_RUN_CRASHED":
398+
case "TASK_RUN_UNCAUGHT_EXCEPTION":
398399
case "TASK_PROCESS_EXITED_WITH_NON_ZERO_CODE":
399400
case "TASK_PROCESS_SIGTERM":
400401
return true;
@@ -425,6 +426,7 @@ export function shouldLookupRetrySettings(error: TaskRunError): boolean {
425426
case "TASK_PROCESS_EXITED_WITH_NON_ZERO_CODE":
426427
case "TASK_PROCESS_SIGTERM":
427428
case "TASK_PROCESS_SIGSEGV":
429+
case "TASK_RUN_UNCAUGHT_EXCEPTION":
428430
return true;
429431

430432
default:

packages/core/src/v3/schemas/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ export const TaskRunInternalError = z.object({
174174
"GRACEFUL_EXIT_TIMEOUT",
175175
"TASK_RUN_HEARTBEAT_TIMEOUT",
176176
"TASK_RUN_CRASHED",
177+
"TASK_RUN_UNCAUGHT_EXCEPTION",
177178
"MAX_DURATION_EXCEEDED",
178179
"DISK_SPACE_EXCEEDED",
179180
"POD_EVICTED",

0 commit comments

Comments
 (0)