defer()
Register inline or durable work on the nearest Crux execution scope.
import {
defer,
CruxDeferError,
DEFER_ERROR_CODES,
type DeferredCallback,
type DeferredWorkRef,
} from "@use-crux/core";defer() has two overloads. It always registers on the nearest compatible
execution scope.
Inline callback
function defer(callback: () => void | PromiseLike<void>): void;The callback starts when its registering scope closes. Agent turns, adapter calls, tool executions, and Safety sessions install scopes automatically. Nested work therefore drains at its own primitive boundary rather than waiting for an enclosing request.
defer(async () => {
await flushAnalytics();
});The callback is process-local and best-effort. Callback failures are contained and observed. A retention-port failure is different: it propagates after the scope seals because the host failed to establish the acceptance guarantee.
Named Runtime target
function defer<TTarget extends RuntimeTaskTarget>(
target: TTarget,
input: RuntimeTaskInput<TTarget>,
): Promise<DeferredWorkRef>;
interface DeferredWorkRef {
readonly kind: "deferred.work";
readonly workId: string;
readonly targetId: string;
}The promise resolves after Runtime durably accepts the JSON-safe input. Named execution waits for logical finalization and outbox release.
const ref = await defer(sendReceipt, { orderId: order.id });Inside an Eval cell, both overloads follow the cell's capture policy: callbacks do not execute, named work does not stage, and the attempted work becomes Eval evidence.
Integration ladder
- Inside a Crux primitive: no wrapper is required. Long-lived processes need no host binding for process-local work.
- On a freezing platform: configure
hostonce. The binding retains an already-started primitive drain to root idle. - At handler root: an ambient-capable binding gives each call a separate ephemeral invocation.
- Strict handler semantics: use a platform wrapper for cross-call grouping, outcome classification, strict response commit, and framework cleanup.
Config-only ambient calls intentionally do not share limits or handler outcome. Each call owns one ephemeral root.
Scope outcome
Inline work registered by a scope that closes with error, cancelled, or
timeout is recorded as skipped and is not invoked. success, redirect, and
not-found drain normally. The gate applies to the registering scope only.
Replayable flow bodies reject public defer() with
DEFER_REPLAY_UNSAFE; use flow.defer().
Host imports
| Import | Application use |
|---|---|
@use-crux/core/defer/node | Node HTTP boundaries and process shutdown |
@use-crux/core/defer/serverless | Provider-neutral strict wrappers, including withServerlessDefer |
@use-crux/next | Next next(), withNextDefer, and withCrux |
@use-crux/vercel | Vercel vercel() retention binding |
@use-crux/cloudflare | Workers workers({ ctx }) and withCrux |
Platform and adapter authors should use the host-adapter contract, not infer capability from environment variables.
Errors
| Code | Cause |
|---|---|
DEFER_SCOPE_REQUIRED | No active scope and no ambient host binding |
DEFER_CAPABILITY_MISSING | The active host cannot honor the overload or retention request |
DEFER_SCOPE_SEALED | Registration arrived after the accepting scope sealed |
DEFER_LIMIT_EXCEEDED | Callback or nesting bounds were exceeded |
DEFER_REPLAY_UNSAFE | Public defer ran inside replayable flow execution |
DEFER_TARGET_INPUT_REQUIRED | Required named input was missing |
DEFER_COMMIT_FAILED | Strict named finalization failed before response commitment |
See Troubleshooting for the decision checklist and exact remediations.