Profiles·Public

@trigger.dev/sdk

semver>=3.0.0 <5.0.0postconditions41functions36last verified2026-06-24coverage score82%

Postconditions: what we check

  • trigger · trigger-no-try-catch
    error
    Whentasks.trigger() called in async context without surrounding try/catch. API failures (auth, rate limit, network) result in unhandled rejected Promises.
    ThrowsApiRequestError (extends Error). status 401: Invalid or missing API key (TRIGGER_SECRET_KEY not set or wrong). status 403: API key lacks permission to trigger the task. status 404: Task with given ID not registered in this environment. status 429: Rate limit exceeded. Error: Network failure (DNS, timeout, connection refused).
    Required handlingCaller MUST wrap tasks.trigger() in try/catch. Handle at minimum: - 404: task not found — may indicate deployment mismatch - 429: rate limit — implement backoff - Network errors: surface to caller rather than silently failing
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[1][2]
  • batchTrigger · batchtrigger-no-try-catch
    error
    Whentasks.batchTrigger() called in async context without surrounding try/catch. API failures result in unhandled rejected Promises.
    ThrowsApiRequestError (extends Error). status 401: Invalid API key. status 429: Rate limit exceeded. Error: Network failure.
    Required handlingCaller MUST wrap tasks.batchTrigger() in try/catch. Handle rate limiting with exponential backoff.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[1][2]
  • retrieve · retrieve-no-try-catch
    error
    Whenruns.retrieve() called in async context without surrounding try/catch. Not-found and auth errors result in unhandled rejected Promises.
    ThrowsApiRequestError (extends Error). status 401: Invalid API key. status 404: Run not found with given ID. Error: Network failure.
    Required handlingCaller MUST wrap runs.retrieve() in try/catch. Handle 404 as graceful not-found — return null instead of throwing.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[3][2]
  • list · list-no-try-catch
    error
    Whenruns.list() called in async context without surrounding try/catch. Auth and network errors result in unhandled rejected Promises.
    ThrowsApiRequestError (extends Error). status 401: Invalid API key. status 400: Invalid filter parameters. status 429: Rate limit exceeded. Error: Network failure.
    Required handlingCaller MUST wrap runs.list() in try/catch. Return empty array on failure rather than crashing.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[4][2]
  • create · schedules-create-no-try-catch
    error
    Whenschedules.create() called in async context without surrounding try/catch. Auth, validation, and network errors result in unhandled rejected Promises.
    ThrowsApiRequestError (extends Error). status 401: Invalid API key. status 422: Invalid schedule parameters (bad cron, unknown task). status 429: Rate limit exceeded. Error: Network failure.
    Required handlingCaller MUST wrap schedules.create() in try/catch. Handle 422 for invalid cron expressions or task IDs.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[5][2]
  • update · schedules-update-no-try-catch
    error
    Whenschedules.update() called in async context without surrounding try/catch. Not-found and auth errors result in unhandled rejected Promises.
    ThrowsApiRequestError (extends Error). status 401: Invalid API key. status 404: Schedule not found with given ID. status 422: Invalid schedule parameters. Error: Network failure.
    Required handlingCaller MUST wrap schedules.update() in try/catch. Handle 404 gracefully — schedule may have been deleted.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[6][2]
  • triggerAndWait · triggerandwait-unwrap-error
    error
    Whentasks.triggerAndWait(...).unwrap() called without try/catch, and the child task fails (throws, crashes, or is canceled). The .unwrap() call throws SubtaskUnwrapError containing taskId, runId, and the child error.
    ThrowsSubtaskUnwrapError (extends Error) with properties: .taskId — the child task identifier .runId — the run ID of the failed child .cause — the error thrown by the child task
    Required handlingCaller MUST wrap .unwrap() in try/catch when child task failure should be handled rather than crashing the parent task. Pattern 1 — check result.ok (no throw): const result = await tasks.triggerAndWait("child-task", payload); if (!result.ok) { console.error("Child task failed:", result.error); return; // or fallback } return result.output; Pattern 2 — use .unwrap() with catch: try { const output = await tasks.triggerAndWait("child-task", payload).unwrap(); } catch (err) { if (err instanceof SubtaskUnwrapError) { console.error("Child task failed:", err.cause); } throw err; } CRITICAL: Do NOT use .unwrap() without try/catch when child failures are expected (e.g., user input validation, external API calls in child).
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[7][8]
  • triggerAndWait · triggerandwait-called-outside-task
    error
    Whentasks.triggerAndWait() called from backend route handler, serverless function, or any context outside a Trigger.dev task run() function. This is explicitly unsupported and throws at runtime.
    ThrowsError: "triggerAndWait can only be called from inside a task" (or similar — the exact message varies by SDK version).
    Required handlingCaller MUST use tasks.trigger() (fire-and-forget) when calling from backend routes. Use tasks.triggerAndWait() ONLY inside a parent task's run() function. The difference: trigger() makes an API call and returns immediately; triggerAndWait() suspends the current task execution until the child task completes — this suspension mechanism only works inside a task.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[7]
  • batchTriggerAndWait · batchtriggerandwait-creation-error
    error
    Whentasks.batchTriggerAndWait() fails during the batch creation phase (before any child tasks start executing). Common causes: rate limiting, invalid API key, task ID not found.
    ThrowsBatchTriggerError (extends Error) with properties: .phase — "create" (failed during batch creation) .isRateLimited — true if caused by HTTP 429 .retryAfterMs — milliseconds until rate limit resets (when isRateLimited) .apiError — the underlying ApiError (auth, not-found, etc.) .itemCount — number of items in the batch When rate limited: BatchTriggerError.isRateLimited === true When auth fails: BatchTriggerError.apiError instanceof AuthenticationError
    Required handlingCaller MUST wrap tasks.batchTriggerAndWait() in try/catch. try { const results = await tasks.batchTriggerAndWait("my-task", items); for (const result of results.runs) { if (!result.ok) { console.error("Item failed:", result.error); } } } catch (err) { if (err instanceof BatchTriggerError && err.isRateLimited) { // Retry after err.retryAfterMs } throw err; } CRITICAL: Even on success, each item in results.runs must be checked individually — some items may fail while others succeed.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[7][9]
  • batchTriggerAndWait · batchtriggerandwait-individual-failures-unchecked
    warning
    Whentasks.batchTriggerAndWait() succeeds (no throw) but individual items in the batch fail. Caller does not check result.runs[i].ok and assumes all items completed successfully. Failed items silently lose their work without any error propagation.
    ThrowsNo throw — individual item failures are returned in results.runs[i] with ok: false. The overall batch call succeeds. results.runs[i] = { ok: false, error: unknown, runId: string }
    Required handlingCaller MUST check each item in results.runs after awaiting: for (const result of results.runs) { if (!result.ok) { // This item failed — handle or re-queue console.error("Batch item failed:", result.error); } } Never assume all items completed successfully after a successful await.
    costmediumin proddegraded serviceusers seedegraded performancevisibilitysilent
    Sources[7]
  • cancel · cancel-no-try-catch
    error
    Whenruns.cancel() called in async context without surrounding try/catch. NotFoundError, AuthenticationError, or BadRequestError results in unhandled rejected Promise.
    ThrowsNotFoundError (status 404) — run not found with given ID. AuthenticationError (status 401) — invalid or missing API key. BadRequestError (status 400) — invalid run ID format. ApiConnectionError — network failure. All extend ApiError with .status and .error properties.
    Required handlingCaller MUST wrap runs.cancel() in try/catch. Handle 404 gracefully — the run may have already completed by the time cancel is called (race condition in cleanup flows). try { await runs.cancel(runId); } catch (err) { if (err instanceof NotFoundError) { // Run already completed or never existed — safe to ignore return; } throw err; }
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[10][11]
  • replay · replay-no-try-catch
    error
    Whenruns.replay() called in async context without surrounding try/catch. NotFoundError or BadRequestError results in unhandled rejected Promise.
    ThrowsNotFoundError (status 404) — run not found with given ID. AuthenticationError (status 401) — invalid or missing API key. BadRequestError (status 400) — invalid run ID or "Failed to create new run". ApiConnectionError — network failure.
    Required handlingCaller MUST wrap runs.replay() in try/catch. Handle 404 as the run may have been purged (TTL expiry is default 30 days). try { const newRun = await runs.replay(runId); console.log("Replayed as:", newRun.id); } catch (err) { if (err instanceof NotFoundError) { console.error("Run not found — may have expired:", runId); } throw err; }
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[12][11]
  • reschedule · reschedule-no-try-catch
    error
    Whenruns.reschedule() called in async context without surrounding try/catch. NotFoundError or AuthenticationError results in unhandled rejected Promise.
    ThrowsNotFoundError (status 404) — run not found or not in DELAYED state. AuthenticationError (status 401) — invalid or missing API key. BadRequestError (status 400) — invalid delay format. ApiConnectionError — network failure.
    Required handlingCaller MUST wrap runs.reschedule() in try/catch. Handle 404 as the run may have already dequeued and started. This race condition is common in high-throughput systems. try { await runs.reschedule(runId, { delay: "5m" }); } catch (err) { if (err instanceof NotFoundError) { // Run already started — reschedule not possible, log and continue console.warn("Could not reschedule run (already started):", runId); } else { throw err; } }
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[13][11]
  • poll · poll-no-try-catch
    warning
    Whenruns.poll() called in async context without surrounding try/catch. Auth errors or network failures during polling cause unhandled rejected Promise.
    ThrowsAuthenticationError (status 401) — invalid API key during polling. ApiConnectionError — network failure during any poll cycle. Does NOT throw when the polled run itself fails — the run failure is returned in the result object (check result.status).
    Required handlingCaller MUST wrap runs.poll() in try/catch. IMPORTANT: runs.poll() has NO built-in timeout. Use an AbortController or external timeout to prevent indefinite polling for stuck runs. const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 300_000); // 5 min max try { const result = await runs.poll(runId, { pollIntervalMs: 2000 }, { signal: controller.signal, }); if (result.status !== "COMPLETED") { console.error("Run did not complete successfully:", result.status); } } catch (err) { console.error("Polling failed:", err); } finally { clearTimeout(timeout); }
    costmediumin proddegraded serviceusers seeservice unavailablevisibilityvisible
    Sources[13][11]
  • pause · pause-no-try-catch
    error
    Whenqueues.pause() called in async context without surrounding try/catch. NotFoundError (404) or AuthenticationError (401) results in unhandled rejected Promise.
    ThrowsNotFoundError (status 404) — queue not found with given ID or name. AuthenticationError (status 401) — invalid or missing API key. BadRequestError (status 400) — invalid queue identifier format. ApiConnectionError — network failure.
    Required handlingCaller MUST wrap queues.pause() in try/catch. Handle 404 gracefully — queue name may be misspelled or queue may not exist yet (lazily created queues may not exist until first run). try { await queues.pause({ type: "task", name: "my-task-id" }); } catch (err) { if (err instanceof NotFoundError) { console.warn("Queue not found — may not exist yet:", err.message); } else { throw err; } }
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[14][11]
  • resume · resume-no-try-catch
    error
    Whenqueues.resume() called in async context without surrounding try/catch. NotFoundError (404) or AuthenticationError (401) results in unhandled rejected Promise.
    ThrowsNotFoundError (status 404) — queue not found with given ID or name. AuthenticationError (status 401) — invalid or missing API key. BadRequestError (status 400) — invalid queue identifier format. ApiConnectionError — network failure.
    Required handlingCaller MUST wrap queues.resume() in try/catch. Same handling pattern as queues.pause().
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[14][11]
  • createToken · createtoken-no-try-catch
    error
    Whenwait.createToken() called in async context without surrounding try/catch. Auth failures or network errors result in unhandled rejected Promise.
    ThrowsAuthenticationError (status 401) — invalid or missing API key. ApiConnectionError — network failure creating the token. RateLimitError (status 429) — rate limit on waitpoint creation.
    Required handlingCaller MUST wrap wait.createToken() in try/catch. A failed token creation means the external callback URL was never created — the workflow pauses without any way to resume. try { const token = await wait.createToken({ idempotencyKey: `approve-${documentId}`, timeout: "24h", }); // Send token.url to external system await notifyApprover(token.url); // Now wait for it (inside task only) const result = await wait.forToken(token); } catch (err) { console.error("Failed to create approval token:", err); throw err; // Abort the task — no way to proceed }
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[15][11]
  • completeToken · completetoken-no-try-catch
    error
    Whenwait.completeToken() called in a webhook handler or API route without try/catch. NotFoundError (token expired or never created) or AuthenticationError results in unhandled rejected Promise. Common in webhook handlers that forget error handling.
    ThrowsNotFoundError (status 404) — token not found (expired, already completed, or token ID is incorrect). AuthenticationError (status 401) — invalid or missing API key. ApiConnectionError — network failure. ConflictError (status 409) — token already completed (idempotent completion attempted twice with different data).
    Required handlingCaller MUST wrap wait.completeToken() in try/catch. In webhook handlers, a 404 is usually safe to ignore (already completed) but should be logged for debugging. // In your webhook handler: try { await wait.completeToken(tokenId, { status: "approved", approvedBy: userId }); } catch (err) { if (err instanceof NotFoundError) { // Token already completed or expired — safe to ignore console.warn("Token already completed:", tokenId); return res.status(200).json({ ok: true }); // Don't retry } console.error("Failed to complete token:", err); return res.status(500).json({ error: "Failed" }); // Allow retry }
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[15][11]
  • createPublicToken · createpublictoken-no-try-catch
    error
    Whenauth.createPublicToken() called in an API route or server action without surrounding try/catch. Auth failure or network error causes unhandled rejected Promise, resulting in a 500 response to the frontend. The frontend receives no token and cannot subscribe to run updates.
    ThrowsApiClientMissingError (synchronous) — TRIGGER_SECRET_KEY not set or configure() not called before auth.createPublicToken(). AuthenticationError (status 401) — API key is invalid or revoked. The generateJWTClaims() HTTP call fails with 401. RateLimitError (status 429) — Token creation rate limit exceeded. ApiConnectionError — Network failure reaching api.trigger.dev.
    Required handlingCaller MUST wrap auth.createPublicToken() in try/catch. // In your API route (e.g., Next.js route handler): try { const token = await auth.createPublicToken({ scopes: { read: { runs: [runId] } }, expirationTime: "15m", }); return Response.json({ token }); } catch (err) { if (err instanceof AuthenticationError) { // TRIGGER_SECRET_KEY is wrong — alert ops team console.error("Trigger.dev auth failed:", err.message); return Response.json({ error: "Internal error" }, { status: 500 }); } throw err; } CRITICAL: If TRIGGER_SECRET_KEY is not set, throws synchronously before any await — a plain try/catch still catches this.
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[16][17]
  • createTriggerPublicToken · createtriggerpublictoken-no-try-catch
    error
    Whenauth.createTriggerPublicToken() called in a backend route without try/catch. Token generation fails (auth, rate limit, or network), causing an unhandled rejected Promise. The frontend trigger button has no token and the trigger call fails.
    ThrowsApiClientMissingError (synchronous) — TRIGGER_SECRET_KEY not set. AuthenticationError (status 401) — API key is invalid or revoked. RateLimitError (status 429) — Rate limit on JWTClaims endpoint. ApiConnectionError — Network failure.
    Required handlingCaller MUST wrap auth.createTriggerPublicToken() in try/catch. try { const token = await auth.createTriggerPublicToken("send-welcome-email"); return Response.json({ token }); } catch (err) { console.error("Failed to create trigger token:", err); return Response.json({ error: "Could not authorize trigger" }, { status: 500 }); } NOTE: Each call to createTriggerPublicToken() is an API network round-trip. Token creation can be rate-limited under high traffic — consider caching tokens with TTL shorter than expirationTime.
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[16][17]
  • createBatchTriggerPublicToken · createbatchtriggerpublictoken-no-try-catch
    error
    Whenauth.createBatchTriggerPublicToken() called in a backend route without try/catch. Failure causes unhandled rejected Promise — frontend cannot batch-trigger any tasks.
    ThrowsApiClientMissingError (synchronous) — TRIGGER_SECRET_KEY not set. AuthenticationError (status 401) — API key invalid. RateLimitError (status 429) — Rate limit on JWTClaims endpoint. ApiConnectionError — Network failure.
    Required handlingCaller MUST wrap auth.createBatchTriggerPublicToken() in try/catch. Same pattern as createTriggerPublicToken — return 500 on failure.
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[16][17]
  • add · tags-add-outside-task-context
    error
    Whentags.add() called from outside a Trigger.dev task run() function — for example, from a Next.js route handler, an Express middleware, or any module-level initialization code. taskContext.ctx is undefined, causing tags.add() to throw immediately.
    ThrowsError: "Can't set tags outside of a run. You can trigger a task and set tags in the options." (Thrown synchronously before any async work)
    Required handlingtags.add() MUST only be called inside a task's run() function. To add tags at trigger time, pass them in the trigger options instead: // CORRECT: pass tags at trigger time (works from anywhere): await tasks.trigger("my-task", payload, { tags: ["user:1234"] }); // CORRECT: add tags dynamically inside the task: export const myTask = task({ id: "my-task", run: async (payload) => { await tags.add(["processing", "user:1234"]); } }); // WRONG: calling tags.add() from a route handler throws: // await tags.add("user:1234") => Error: "Can't set tags outside of a run"
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[18][19]
  • add · tags-add-no-try-catch
    warning
    Whentags.add() called inside a task without surrounding try/catch. Network failure or auth error during the tag API call causes the entire task run to fail — not just the tagging operation.
    ThrowsAuthenticationError (status 401) — API key invalid during tag write. ApiConnectionError — Network failure during tag write. RateLimitError (status 429) — Rate limit on tag updates.
    Required handlingIf tag failures should not abort the entire task, wrap in try/catch: try { await tags.add(["processing", `user:${userId}`]); } catch (err) { // Tags are non-critical — log and continue console.warn("Failed to add tags:", err); } If tags are critical for observability/routing, let it throw and fail the task so the run is retried with correct tags applied.
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[18][19]
  • flush · metadata-flush-silent-failure
    warning
    Whenmetadata.flush() is called and the underlying API call fails (network error, auth failure, rate limit). The caller assumes the flush succeeded because no error is thrown. Metadata operations queued before the flush (set, append, increment, etc.) are permanently lost — never persisted to the database. Child tasks triggered after the apparent "flush" may read stale or empty metadata from the parent.
    ThrowsNo throw — metadata.flush() catches ALL errors internally and logs them. The function always resolves (never rejects). Error is only visible in process logs as: "Failed to flush metadata <error details>" Callers CANNOT detect failure via try/catch.
    Required handlingIMPORTANT: You cannot rely on try/catch to detect metadata.flush() failures. The flush either succeeds or silently fails with only a console.error log. To maximize reliability of metadata persistence: 1. Use idempotent metadata operations (set with fixed keys, not append to growing arrays) — safe to retry automatically by the periodic flush. 2. Log metadata state before flushing for debugging: console.log("Flushing metadata:", metadata.current()); await metadata.flush(); 3. For CRITICAL metadata that child tasks depend on, pass it as part of the task payload instead — payload is durably stored at trigger time. 4. The periodic auto-flush (every 1s) retries failed flushes — a transient network error during an explicit flush may succeed in the next automatic flush cycle. // MISLEADING PATTERN: this try/catch does NOT catch flush failures: try { await metadata.flush(); // Never throws even on failure } catch (err) { // This block never executes on flush failure — swallowed internally }
    costlowin proddegraded serviceusers seedegraded performancevisibilitysilent
    Sources[20][21]
  • constructEvent · constructevent-no-try-catch
    error
    Whenwebhooks.constructEvent() called in a webhook handler without surrounding try/catch. Any of five distinct failure modes (missing secret, missing signature header, array header, invalid signature, malformed payload) results in an unhandled rejected Promise that crashes the handler with a 500.
    ThrowsWebhookError (extends Error) — single error class for ALL failures: - "Secret is required when passing a Request object" - "No signature header found" (missing x-trigger-signature-hmacsha256) - "Signature header cannot be an array" - "Invalid signature" (HMAC-SHA256 verification failed) - "Webhook parsing failed: <inner zod/JSON message>" Distinguishable only by the message string — WebhookError does not expose a discriminator code or type field.
    Required handlingCaller MUST wrap webhooks.constructEvent() in try/catch and return a non-2xx response on ANY error. NEVER catch-and-return-200, because that allows attackers with no signing secret to deliver arbitrary payloads to the handler. // CORRECT — Next.js route handler pattern: export async function POST(request: Request) { try { const event = await webhooks.constructEvent(request, process.env.TRIGGER_WEBHOOK_SECRET!); // event is now a verified Webhook — safe to process await processVerifiedWebhook(event); return Response.json({ ok: true }); } catch (err) { if (err instanceof WebhookError) { console.warn("Rejected unverified webhook:", err.message); return new Response("Invalid webhook", { status: 400 }); } throw err; } } // WRONG — silently treats forged webhooks as authentic: // try { event = await webhooks.constructEvent(request, secret); } // catch { return new Response("ok"); } // 200 on forge — vulnerability
    costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[22]
  • constructEvent · constructevent-swallowed-returns-200
    error
    Whenwebhooks.constructEvent() is wrapped in a try/catch where the catch block returns a 2xx response (e.g., Response.json({ok: true})). This completely defeats signature verification — any unauthenticated POST to the webhook URL receives a 200, signaling Trigger.dev (and any downstream consumers) that the event was accepted. Attackers can forge events with arbitrary payloads.
    ThrowsNo throw — but the application returns 200 for unverified webhooks, which is a critical authentication bypass.
    Required handlingThe catch block for WebhookError MUST return a non-2xx response (400 or 401 are appropriate). Logging and continuing without throwing is fine, but the HTTP response itself must signal rejection. // WRONG (vulnerability): try { event = await webhooks.constructEvent(req, secret); } catch (err) { console.error(err); return Response.json({ ok: true }); // 200 — attacker bypass } // RIGHT: try { event = await webhooks.constructEvent(req, secret); } catch (err) { return new Response("Invalid webhook", { status: 400 }); }
    costhighin prodsilent failureusers seelost datavisibilitysilent
    Sources[22]
  • forToken · fortoken-called-outside-task
    error
    Whenwait.forToken() called from outside a Trigger.dev task run() function — e.g., from a Next.js route handler, an Express middleware, or module-level code. The SDK throws synchronously, before any await.
    ThrowsError: "wait.forToken can only be used from inside a task.run()" (Synchronous throw at the top of the implementation — does not return a rejected Promise.)
    Required handlingwait.forToken() is a task-internal API. To wait for external confirmation from a non-task context, you must: 1. Create the token from anywhere with wait.createToken(). 2. Inside a task, await wait.forToken(token) to pause the run. 3. From the external context (webhook, route handler), call wait.completeToken(tokenId, data) to unblock the task. // CORRECT (inside a task): export const approvalTask = task({ id: "approval-flow", run: async (payload) => { const token = await wait.createToken({ timeout: "24h" }); await notifyApprover(token.url); const result = await wait.forToken<{ approved: boolean }>(token); if (!result.ok) { throw result.error; } return result.output; } }); // WRONG (route handler): // export async function GET() { // const result = await wait.forToken(tokenId); // throws synchronously // }
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[23][15]
  • forToken · fortoken-unwrap-timeout-unhandled
    error
    Whenwait.forToken(token).unwrap() called inside a task without surrounding try/catch. When the timeout expires before wait.completeToken() is called externally, unwrap() throws WaitpointTimeoutError. The whole task run fails and is retried per the task's retry config — potentially burning quota for a token that will never be completed (e.g., the user abandoned the approval flow).
    ThrowsWaitpointTimeoutError (extends Error) — thrown by .unwrap() when the underlying result is { ok: false, error: WaitpointTimeoutError }. The error has a generic message indicating the timeout fired.
    Required handlingFor abandonment-tolerant flows (human approval, async sync, etc.): - Either check result.ok BEFORE unwrapping, OR - Wrap .unwrap() in try/catch and treat WaitpointTimeoutError as a normal control-flow exit (return a fallback / skip the step). // PATTERN 1: explicit check (recommended for abandonment flows): const result = await wait.forToken<ApprovalData>(token); if (!result.ok) { // Approval timed out — mark as rejected, don't retry the task await markAbandoned(payload.documentId); return { status: "abandoned" }; } // PATTERN 2: unwrap with explicit catch: try { const approval = await wait.forToken<ApprovalData>(token).unwrap(); return { approved: true, by: approval.userId }; } catch (err) { if (err instanceof WaitpointTimeoutError) { return { status: "timeout" }; } throw err; } CRITICAL: Without explicit timeout handling, abandoned waitpoints cause repeated task retries that all hit the same timeout — wasting concurrency slots and run quota.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[23][15]
  • execute · query-execute-no-try-catch
    error
    Whenquery.execute() called in async context (route handler, scheduled report job, dashboard backend) without surrounding try/catch. Auth failure, malformed TRQL, or invalid time range result in an unhandled rejected Promise — the report endpoint returns 500 instead of a graceful empty/error response.
    ThrowsApiClientMissingError (synchronous) — TRIGGER_SECRET_KEY not set. AuthenticationError (status 401) — invalid or expired API key. BadRequestError (status 400) — invalid TRQL syntax, unknown table, or invalid time range (mixing `period` with `from`/`to`). RateLimitError (status 429) — query rate limit exceeded. ApiConnectionError — network failure reaching api.trigger.dev.
    Required handlingCaller MUST wrap query.execute() in try/catch. Differentiate BadRequestError (developer error, do not retry) from ApiConnectionError / RateLimitError (transient, may retry). try { const result = await query.execute( "SELECT run_id, status FROM runs", { period: "7d", format: "json" } ); return Response.json({ runs: result.results }); } catch (err) { if (err instanceof BadRequestError) { // TRQL syntax error or invalid params — log and return 400 console.error("Invalid TRQL:", err.message); return new Response("Invalid query", { status: 400 }); } if (err instanceof RateLimitError) { return new Response("Too many requests", { status: 429 }); } throw err; } NOTE: query.execute() can fan out across organization-scoped data when scope: "organization" — large result sets may also fail with server-side timeouts that surface as ApiConnectionError.
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[24][11]
  • envvars-upload · envvars-upload-no-try-catch
    error
    Whenenvvars.upload() called in async context without surrounding try/catch. Arg-validation throws synchronously when projectRef/slug/params are missing (deployment-pipeline misconfiguration), and the network upload can fail with AuthenticationError, RateLimitError, BadRequestError, or ApiConnectionError. When `override: true`, a half-applied failure can leave the environment in a partially-replaced state — some vars updated, others stale.
    ThrowsError (synchronous) — "params is required", "slug is required", "projectRef is required" (when called via overload that requires them). ApiClientMissingError (synchronous) — TRIGGER_SECRET_KEY not set. AuthenticationError (status 401) — invalid or revoked Personal Access Token. BadRequestError (status 400) — malformed env var names (e.g. starting with a digit), reserved key collisions (TRIGGER_*). RateLimitError (status 429) — bulk import rate limit. ApiConnectionError — network failure mid-upload.
    Required handlingCaller MUST wrap envvars.upload() in try/catch — typically in a deploy script or CI step. Distinguish BadRequestError (developer error: fix the var names, do not retry) from RateLimitError / ApiConnectionError (transient: retry with exponential backoff). try { await envvars.upload(projectRef, "prod", { variables: { DATABASE_URL: process.env.NEW_DB_URL, ... }, override: true, }); } catch (err) { if (err instanceof BadRequestError) { console.error("Invalid env var spec — fix and re-run:", err.message); process.exit(2); } if (err instanceof AuthenticationError) { console.error("TRIGGER_ACCESS_TOKEN invalid — rotate and re-run"); process.exit(3); } throw err; } CRITICAL: when `override: true`, a mid-upload failure can leave the environment partially replaced. Re-run the upload to make it idempotent, or set override: false and patch individual vars.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[25][26]
  • envvars-create · envvars-create-no-try-catch
    error
    Whenenvvars.create() called in async context without surrounding try/catch. Synchronous arg-validation throws "params is required"/"slug is required"/ "projectRef is required", and the network call can fail with auth, conflict (variable already exists), or validation errors.
    ThrowsError (synchronous) — "params is required", "slug is required", "projectRef is required" (when required overload arg is missing). ApiClientMissingError (synchronous) — TRIGGER_SECRET_KEY not set. AuthenticationError (status 401) — invalid token. ConflictError (status 409) — variable with this name already exists. BadRequestError (status 400) — invalid name (reserved prefix, illegal chars). ApiConnectionError — network failure.
    Required handlingCaller MUST wrap envvars.create() in try/catch. The most common operational error is ConflictError when an env var already exists — surface this as a "already exists, use update() instead" message rather than crashing the deploy. try { await envvars.create({ name: "STRIPE_KEY", value: process.env.STRIPE_KEY }); } catch (err) { if (err instanceof ConflictError) { await envvars.update("STRIPE_KEY", { value: process.env.STRIPE_KEY }); return; } throw err; }
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[27][26]
  • envvars-update · envvars-update-no-try-catch
    error
    Whenenvvars.update() called in async context without surrounding try/catch. NotFoundError (404) when the variable doesn't exist, AuthenticationError on bad token, BadRequestError on invalid value (e.g. exceeding size limits).
    ThrowsError (synchronous) — "params is required", "name is required", "slug is required", "projectRef is required" (missing required overload arg). ApiClientMissingError (synchronous) — TRIGGER_SECRET_KEY not set. AuthenticationError (status 401) — invalid token. NotFoundError (status 404) — variable with given name doesn't exist on this environment. BadRequestError (status 400) — value exceeds size limit or invalid format. ApiConnectionError — network failure.
    Required handlingCaller MUST wrap envvars.update() in try/catch. Handle 404 as an opportunity to upsert via create() rather than crashing — common pattern in deploy scripts. try { await envvars.update("STRIPE_KEY", { value: process.env.STRIPE_KEY }); } catch (err) { if (err instanceof NotFoundError) { await envvars.create({ name: "STRIPE_KEY", value: process.env.STRIPE_KEY }); return; } throw err; }
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[28][26]
  • envvars-del · envvars-del-no-try-catch
    error
    Whenenvvars.del() called in async context without surrounding try/catch. NotFoundError (404) means the variable doesn't exist; idempotent delete callers should typically treat this as success, but failing to catch NotFoundError will crash the calling deploy/cleanup script.
    ThrowsError (synchronous) — "name is required", "slug is required", "projectRef is required" (missing required overload arg). ApiClientMissingError (synchronous) — TRIGGER_SECRET_KEY not set. AuthenticationError (status 401) — invalid token. NotFoundError (status 404) — variable doesn't exist (already deleted, or name is misspelled). ApiConnectionError — network failure.
    Required handlingCaller MUST wrap envvars.del() in try/catch. For idempotent cleanup flows, treat NotFoundError as success and continue. For audit-tracked deletes (compliance/secret-rotation), log NotFoundError to detect drift. try { await envvars.del("OLD_STRIPE_KEY"); } catch (err) { if (err instanceof NotFoundError) { console.log("OLD_STRIPE_KEY already removed — idempotent OK"); return; } throw err; }
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[29][26]
  • prompts-resolve · prompts-resolve-no-try-catch
    error
    Whenprompts.resolve() called in async context without surrounding try/catch. When the prompt is missing (NotFoundError) or the prompts API is unreachable (ApiConnectionError), the AI task that follows has no prompt and will fail or — worse — send an empty/undefined prompt to the LLM, producing nonsense completions that silently degrade output quality.
    ThrowsApiClientMissingError (synchronous) — TRIGGER_SECRET_KEY not set. AuthenticationError (status 401) — invalid token. NotFoundError (status 404) — prompt slug doesn't exist, or the requested version/label is missing. BadRequestError (status 400) — missing required variables that the template expects, or invalid variable shape. ApiConnectionError — network failure reaching api.trigger.dev.
    Required handlingCaller MUST wrap prompts.resolve() in try/catch. AI pipelines should either fail fast (preferred — emit a clear "prompt not found" error to the operator) or fall back to a pinned in-code prompt with explicit observability marking the fallback. NEVER catch-and-continue with an empty/undefined prompt. try { const { compiled } = await prompts.resolve("summarize-email", { tone: "warm" }); const completion = await openai.chat.completions.create({ model: "gpt-4o", messages: [{ role: "user", content: compiled }], }); } catch (err) { if (err instanceof NotFoundError) { await alertOnCall("Prompt 'summarize-email' missing from Trigger.dev"); throw new Error("Cannot proceed without prompt definition"); } throw err; }
    costmediumin proddegraded serviceusers seedegraded performancevisibilitysilent
    Sources[30]
  • prompts-promote · prompts-promote-no-try-catch
    error
    Whenprompts.promote() called in async context without surrounding try/catch. NotFoundError (slug or version missing) or BadRequestError causes unhandled rejection, but more critically — a SUCCESSFUL promote of the wrong version silently swaps the prompt active across all running AI tasks. Catching errors is the minimum; logging the before/after version is recommended.
    ThrowsApiClientMissingError (synchronous) — TRIGGER_SECRET_KEY not set. AuthenticationError (status 401) — invalid token. NotFoundError (status 404) — slug or version doesn't exist. BadRequestError (status 400) — version is not a number, or is below the minimum allowed. ApiConnectionError — network failure.
    Required handlingCaller MUST wrap prompts.promote() in try/catch AND log the version transition for audit. Promotes are immediately effective — no soft-deploy. try { const before = await prompts.list(); await prompts.promote("summarize-email", 7); console.log("Promoted summarize-email v? -> v7", { before }); } catch (err) { if (err instanceof NotFoundError) { console.error("Cannot promote: slug or version missing", err.message); } throw err; }
    costmediumin prodsilent failureusers seedegraded performancevisibilitysilent
    Sources[30]
  • prompts-create-override · prompts-create-override-no-try-catch
    error
    Whenprompts.createOverride() called in async context without surrounding try/catch. ConflictError when an override already exists, NotFoundError if slug doesn't exist, BadRequestError on malformed body.
    ThrowsApiClientMissingError (synchronous) — TRIGGER_SECRET_KEY not set. AuthenticationError (status 401) — invalid token. NotFoundError (status 404) — slug doesn't exist. ConflictError (status 409) — override already exists for this slug. BadRequestError (status 400) — malformed override body, missing required fields. ApiConnectionError — network failure.
    Required handlingCaller MUST wrap prompts.createOverride() in try/catch. Most operational callers should attempt create-or-update — handle ConflictError by calling updateOverride. try { await prompts.createOverride("summarize-email", { content: newText }); } catch (err) { if (err instanceof ConflictError) { await prompts.updateOverride("summarize-email", { content: newText }); return; } throw err; }
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[30]
  • idempotency-keys-create · idempotency-keys-create-no-try-catch
    warning
    WhenidempotencyKeys.create() called in async context without surrounding try/catch. While operational errors are rare, the SDK looks up taskContext.ctx?.run.id internally — a crypto subsystem failure or context-mismatch can reject the promise. More importantly, if the caller treats this as synchronous (forgets the await), the IdempotencyKey is a Promise object passed to .trigger({ idempotencyKey: ... }) and the API rejects with a confusing "key must be a string" error, defeating idempotency entirely and allowing duplicate child runs on parent retry.
    ThrowsOperationError / Error — crypto subsystem failure (edge/browser SubtleCrypto). Synchronous TypeError — when called with non-string non-array input (developer error caught at call site).
    Required handlingAlways await idempotencyKeys.create() and wrap in try/catch in retry-sensitive paths. The key purpose of this call is to prevent duplicate child runs on parent retry — if creation fails silently and the caller passes an undefined idempotency key, the subsequent retry will trigger a duplicate child task. try { const key = await idempotencyKeys.create(["user", userId, "welcome"]); await childTask.trigger(payload, { idempotencyKey: key }); } catch (err) { console.error("Idempotency key creation failed — refusing to trigger duplicate", err); throw err; }
    costmediumin prodsilent failureusers seelost transactionvisibilitysilent
    Sources[31][32]
  • streams-read · streams-read-no-try-catch
    error
    Whenstreams.read() called in async context without surrounding try/catch. The initial subscription handshake can fail (auth, network, run-not-found), but more subtly — iteration of the returned AsyncIterableStream can throw mid-stream when the connection drops. Production dashboards that pipe streams to the browser crash silently if the iterator's errors aren't caught.
    ThrowsApiClientMissingError (synchronous) — TRIGGER_SECRET_KEY not set. AuthenticationError (status 401) — invalid token. NotFoundError (status 404) — runId doesn't exist or has no stream with the given key. ApiConnectionError — initial handshake network failure, OR mid-stream connection drop (surfaced via the iterator's next() rejection).
    Required handlingCaller MUST wrap both the subscription AND the iteration loop in try/catch. Mid-stream errors surface only at the for-await level — a try/catch around only streams.read() will miss connection drops. try { const stream = await streams.read<Chunk>(runId, "completion"); try { for await (const chunk of stream) { process(chunk); } } catch (iterErr) { console.warn("Stream dropped mid-flight:", iterErr); // Optionally: reconnect via streams.read again } } catch (err) { if (err instanceof NotFoundError) { return; // Run has no stream — caller may need to wait } throw err; }
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[33][34]
  • streams-append · streams-append-no-try-catch
    warning
    Whenstreams.append() called outside a task without an explicit `target` option, or called inside a task with a transient API error. The synchronous target-run-id error throws BEFORE returning a Promise — a try/catch is still required because the synchronous throw will reject the surrounding async function. Mid-flight RateLimitError causes partial stream loss — chunks before the error reach the consumer, chunks after do not, but the consumer has no signal that the stream is now truncated.
    ThrowsError (synchronous) — "Could not determine the target run ID for the realtime stream. Please specify a target run ID using the `target` option or use this function from inside a task." ApiClientMissingError (synchronous) — TRIGGER_SECRET_KEY not set. AuthenticationError (status 401) — invalid token. RateLimitError (status 429) — per-run stream-write rate limit. ApiConnectionError — network failure during write.
    Required handlingCaller MUST wrap streams.append() in try/catch. For high-throughput LLM streaming use cases, RateLimitError is the dominant failure mode — handle by buffering and retrying after the indicated retry-after delay, NOT by dropping chunks (the consumer dashboard will display partial output). for await (const chunk of openaiStream) { try { await streams.append("completion", chunk); } catch (err) { if (err instanceof RateLimitError) { await sleep(err.retryAfterMs); await streams.append("completion", chunk); // single retry } else { throw err; } } }
    costlowin proddegraded serviceusers seedegraded performancevisibilitysilent
    Sources[33][34]
  • batch-retrieve · batch-retrieve-no-try-catch
    error
    Whenbatch.retrieve() called in async context without surrounding try/catch. NotFoundError (404) when the batch ID doesn't exist or was purged (default 30-day TTL), AuthenticationError on bad token, ApiConnectionError on network failure.
    ThrowsApiClientMissingError (synchronous) — TRIGGER_SECRET_KEY not set. AuthenticationError (status 401) — invalid token. NotFoundError (status 404) — batchId doesn't exist or has expired. ApiConnectionError — network failure.
    Required handlingCaller MUST wrap batch.retrieve() in try/catch. Treat NotFoundError as a terminal "batch unknown" — do not retry. Treat ApiConnectionError as transient and retry with exponential backoff. try { const batchInfo = await batch.retrieve(batchId); // CRITICAL: always check per-item status before declaring success const failed = batchInfo.runs.filter(r => r.status !== "COMPLETED"); if (failed.length > 0) { console.warn(`${failed.length}/${batchInfo.runs.length} items failed`); } } catch (err) { if (err instanceof NotFoundError) { return null; // batch unknown — surface to caller as "not found" } throw err; }
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[35][36]
  • usage-measure · usage-measure-no-try-catch
    warning
    Whenusage.measure() called without try/catch around an inner callback that can throw. The compute span is closed correctly, but the throw bubbles out of measure() and crashes the surrounding task code. Callers writing "I'll handle errors in the callback" are misled — measure() does not catch them, it propagates them.
    ThrowsWhatever the inner callback throws — propagated unchanged after closing the compute span.
    Required handlingCaller MUST wrap usage.measure() in try/catch when the inner callback can throw and the task should continue on failure. The compute measurement is still captured even if the callback throws — read it from the error path via a finally block if needed. try { const { result, compute } = await usage.measure(async () => { return await expensiveLLMCall(); }); console.log("LLM call cost", compute.costInCents); return result; } catch (err) { console.error("Measured call failed:", err); throw err; }
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[37][38]

Sources

Every postcondition cites at least one of these. Grouped by source type; numbered to match the footnotes above.

Official documentation
  • [1]
    trigger.dev/docs/management/runs
    Trigger
  • [2]
    trigger.dev/docs/errors-retrying
    Errors Retrying
  • [3]
    trigger.dev/docs/management/runs
    Retrieve
  • [4]
    trigger.dev/docs/management/runs
    List
  • [5]
    trigger.dev/docs/management/schedules
    Create
  • [6]
    trigger.dev/docs/management/schedules
    Update
  • [7]
    trigger.dev/docs/triggering
    Triggering
  • [10]
    trigger.dev/docs/management/runs
    Cancel
  • [12]
    trigger.dev/docs/management/runs
    Replay
  • [14]
    trigger.dev/docs/management/queues
    Pause
  • [16]
    trigger.dev/docs/frontend/overview
    Overview
  • [18]
    trigger.dev/docs/tags
    Tags
  • [20]
    trigger.dev/docs/runs/metadata
    Metadata
  • [23]
    trigger.dev/docs/wait-for-token
    Wait For Token
  • [25]
    trigger.dev/docs/management/envvars
    Import
  • [27]
    trigger.dev/docs/management/envvars
    Create
  • [28]
    trigger.dev/docs/management/envvars
    Update
  • [29]
    trigger.dev/docs/management/envvars
    Delete
  • [31]
    trigger.dev/docs/idempotency
    Idempotency
  • [33]
    trigger.dev/docs/realtime/streams
    Streams
  • [35]
    trigger.dev/docs/management/batch
    Retrieve
  • [37]
    trigger.dev/docs/run-usage
    Run Usage
Source code

Research notes

Curator notes from SOURCES.md captured when the profile was written so you can verify the reasoning, not just the rules.

Sources: @trigger.dev/sdk

Official Documentation

Error Handling

Run Management

Schedule Management

Error Type Evidence

The SDK throws ApiRequestError (extends Error) for all HTTP errors. From SDK source and docs:

  • 401: Invalid or missing TRIGGER_SECRET_KEY environment variable
  • 403: API key lacks required permissions
  • 404: Run/task/schedule not found
  • 422: Validation error (invalid cron expression, unknown task ID)
  • 429: Rate limit exceeded — common in high-throughput job triggering
  • 500/503: Trigger.dev service error

Real-World Evidence

recoupable--api (local test repo)

Found in lib/trigger/:

  • triggerRunSandboxCommand.ts:18tasks.trigger("run-sandbox-command", payload) without try-catch
  • createSchedule.ts:21schedules.create({...}) without try-catch
  • updateSchedule.ts:18schedules.update(scheduleId, {...}) without try-catch
  • retrieveTaskRun.ts:10runs.retrieve(runId) without try-catch
  • listTaskRuns.ts:22runs.list({tag: [tag], limit}) without try-catch

All 5 call sites lack try-catch. Strong evidence of common misuse pattern.

Need a different package?
Request a profile