Troubleshooting
Diagnose defer scope, retention, capability, and durable-acceptance failures.
Public defer failures use CruxDeferError with a stable code and docsUrl:
import { CruxDeferError, defer } from "@use-crux/core";
try {
defer(() => {});
} catch (error) {
if (error instanceof CruxDeferError) {
console.error(error.code, error.docsUrl);
}
}Start with the ladder
- Is the call inside a Crux primitive? Agent turns, adapter calls, tools, and Safety sessions open scopes automatically.
- Can the process stay alive? On Next or Vercel, add the platform binding
to
config({ host }). On Workers, use a per-request boundary withctx. - Is the call at handler root? An ambient-capable config binding gives each root-level call an ephemeral invocation.
- Does the handler outcome or commit barrier matter? Add the platform wrapper for one grouped, classified invocation.
- Is this a replayable flow? Use
flow.defer(), never publicdefer().
Error map
| Code | Meaning | First fix |
|---|---|---|
DEFER_SCOPE_REQUIRED | No execution scope and no ambient host binding | Move inside a Crux primitive or configure the platform host |
DEFER_CAPABILITY_MISSING | The active host cannot honor this overload or cannot retain it | Supply after/waitUntil/Workers ctx, or use named work on a named-only host |
DEFER_SCOPE_SEALED | Registration arrived after the accepting scope sealed | Register before the boundary returns; do not leak scope-bound callbacks |
DEFER_LIMIT_EXCEEDED | Callback or nesting limits were exceeded | Reduce fan-out/nesting or move durable fan-out into Runtime work |
DEFER_REPLAY_UNSAFE | Public defer() ran inside replayable flow execution | Use flow.defer() |
DEFER_TARGET_INPUT_REQUIRED | A named target has no required JSON input | Pass the target's inferred input |
DEFER_COMMIT_FAILED | Strict named staging/finalization failed before response commitment | Fix Runtime acceptance; catching the named promise cannot restore success |
Runtime-bound failures may also use Runtime codes such as
RUNTIME_REQUIRED.
Common cases
It works locally but disappears on serverless
The local process stays alive; the deployment freezes. Install the platform package and configure its binding:
export default config({ host: next() });For Workers, pass the current request's ctx through withCrux or a custom
per-request binding. Never reuse another request's context.
A retention call throws after my function returned
The scope seals before the original retention error propagates. This is an acceptance failure, not a callback failure: Crux cannot promise the host will keep accepted work alive. The already-started drain remains best-effort.
Named work stages but the response is not protected
Config-only ambient mode makes your await defer(target, input) the acceptance
barrier. It does not classify the handler outcome or poison a later response.
Use a strict wrapper with durable finalization when response commitment depends
on the named work.
Project checks
crux setup inspects source and package evidence. On a detected freezing
platform it recommends the exact config({ host: ... }) line when no binding or
strict wrapper is proven. crux setup --apply only applies safe additive
Runtime resources; host integration remains an explicit code change.
Project Index may report:
defer.replay_unsafedefer.floating_named_promiseruntime.missing_runtime_config
Bundled lints report only properties decidable from source. Run
crux setup --check for config- and environment-dependent host retention;
runtime capability checks remain authoritative.