Skip to content

Commit a5837fc

Browse files
committed
docs(troubleshooting): explain TASK_RUN_UNCAUGHT_EXCEPTION
Adds an "Uncaught exceptions" section that the dashboard's pretty-link button now points at (#uncaught-exceptions). Covers what the error means, the common EventEmitter-without-listener cause (with a node-redis example), the .on("error", ...) fix, and the unhandledRejection path.
1 parent c672d33 commit a5837fc

1 file changed

Lines changed: 49 additions & 0 deletions

File tree

docs/troubleshooting.mdx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,55 @@ You could also offload the CPU-heavy work to a Node.js worker thread, but this i
278278

279279
If the above doesn't work, then we recommend you try increasing the machine size of your task. See our [machines guide](/machines) for more information.
280280

281+
### Uncaught exceptions
282+
283+
If you see a `TASK_RUN_UNCAUGHT_EXCEPTION` error, an exception escaped your task's `run()` function without being thrown through your `await` chain — the runtime caught it via Node's `process.on("uncaughtException")` handler. The dashboard surfaces this as a regular task failure (status `Failed`) and the run will retry according to your task's retry policy, but the exception still indicates a bug worth fixing.
284+
285+
The most common cause is a Node `EventEmitter` emitting an `"error"` event with no listener attached. When this happens, Node escalates the event into an `uncaughtException`. Long-lived clients like `node-redis`, `pg`, `kafkajs`, and `mongodb` all surface socket-level errors this way.
286+
287+
For example, a `node-redis` client with no error listener will fail your run with an `Error: read ECONNRESET` (or similar TCP error) the next time the socket is reset:
288+
289+
```ts
290+
import { task } from "@trigger.dev/sdk";
291+
import { createClient } from "redis";
292+
293+
export const myTask = task({
294+
id: "my-task",
295+
run: async () => {
296+
const client = createClient({ url: process.env.REDIS_URL });
297+
298+
// BAD: no .on("error", ...) listener — a socket reset will crash the run
299+
// with an uncaught exception, even if the next .get() would have worked.
300+
await client.connect();
301+
return await client.get("foo");
302+
},
303+
});
304+
```
305+
306+
Fix it by attaching an `error` listener so the event has somewhere to go:
307+
308+
```ts
309+
const client = createClient({ url: process.env.REDIS_URL });
310+
311+
// GOOD: the listener catches socket-level errors. The awaited command
312+
// (e.g. .get) will still reject if the connection is broken, and that
313+
// rejection propagates through your run() and fails the attempt cleanly.
314+
client.on("error", (err) => {
315+
logger.warn("Redis client error", { err });
316+
});
317+
318+
await client.connect();
319+
return await client.get("foo");
320+
```
321+
322+
The same fix applies to any library that emits `"error"` events. As a rule, attach an `.on("error", ...)` listener to every long-lived client you create inside a task.
323+
324+
<Note>
325+
326+
Unhandled promise rejections (e.g. `Promise.reject(...)` with no `.catch`) take the same path — Node routes them through `uncaughtException` by default, and the runtime treats them as `TASK_RUN_UNCAUGHT_EXCEPTION` for the same reasons. Make sure every promise either gets `await`ed or has a `.catch(...)` handler.
327+
328+
</Note>
329+
281330
### Realtime stream error (`sendBatchNonBlocking` / `S2AppendSession`)
282331

283332
Errors mentioning `sendBatchNonBlocking`, `@s2-dev/streamstore`, or `S2AppendSession` (often with `code: undefined`) can occur when you close a stream and then await `waitUntilComplete()`, or when a stream runs for a long time (e.g. 20+ minutes). Wrap `waitUntilComplete()` in try/catch so Transport/closed-stream errors don't fail your task:

0 commit comments

Comments
 (0)