API reference@evolu/commonTask › daemon

function daemon<T, E, D>(task: Task<T, E, D>): Task<T, AbortError | E, D>;

Defined in: packages/common/src/Task.ts:4809

Starts a Task with Run.daemon and waits until it settles or the current Run aborts.

When the current Run aborts, this helper requests abort for the daemon Task and returns AbortError without waiting for that daemon Task to observe abort, clean up, or settle. This makes the wait abortable, not the underlying execution. The daemon Task continues under root Run ownership until it settles, observes abort, or the root Run is disposed.

This is not a replacement for direct AbortSignal support in operations that can observe abort, such as fetch, timers that accept a signal, or callback APIs that accept a signal. Use it as an escape hatch for Tasks that ignore abort when an abort request must stop waiting immediately.

Do not wrap a Task that keeps using a resource the caller may release after this wrapper returns, unless the Task reliably observes abort before using that resource. The daemon Task can continue after the caller stops waiting. Later domain Err results from the daemon Task are discarded after the caller stops waiting. Defects from the daemon Task remain visible to the root Run: if it later throws or rejects, the root Run still panics and reports the defect.

Compose with race or timeout when the losing Task must not delay the winner. Those helpers normally abort losing Tasks and wait for them to settle, keeping cleanup and late defects inside the caller's lifetime. A Task that ignores abort can keep them waiting.

run.abortable(task) returns an owned child Fiber and requests abort through that Fiber; daemon(task) starts a daemon child and stops waiting when the current Run aborts. unabortable masks abort for a Task that must finish once started; daemon lets a Task outlive the caller.

Because the Task starts with Run.daemon, a recorded abort request returns AbortError before the Task starts — including a request masked by unabortable, even though run.signal stays un-aborted inside the mask. Inside a masked body, wrapping a Task with daemon opts the wait back into abort observation; omit the wrapper when the mask should keep the Task running.

Example

import { createRun, daemon, ok, timeout, type Task } from "@evolu/common";

let finished = false;
let finishTask = (): void => {};
const taskNotUsingAbort: Task<string> = () =>
  new Promise((resolve) => {
    finishTask = () => {
      finished = true;
      resolve(ok("done"));
    };
  });

{
  await using run = createRun();
  const result = await run(timeout(daemon(taskNotUsingAbort), "1ms"));
  assert(!result.ok);
  expect(result.error.type).toBe("TimeoutError");
  expect(finished).toBe(false);
  finishTask();
}
expect(finished).toBe(true);

Promise-producing operations should start inside the Task, not before it.

import { createRun, ok, type Result, type Task } from "@evolu/common";

type ResultValue = string;
interface MyError {
  readonly type: "MyError";
}
const createPromiseReturningResult = (): Promise<
  Result<ResultValue, MyError>
> => Promise.resolve(ok("value"));

const task: Task<ResultValue, MyError> = () => createPromiseReturningResult();

await using run = createRun();
expectOk(await run(task), "value");

Do not reuse an already-running Promise. It started outside the Task, so the Run cannot own its lifetime or request abort before it begins.

import { ok, type Result, type Task } from "@evolu/common";

type ResultValue = string;
interface MyError {
  readonly type: "MyError";
}
let promiseStarted = false;
const createPromiseReturningResult = (): Promise<
  Result<ResultValue, MyError>
> => {
  promiseStarted = true;
  return Promise.resolve(ok("value"));
};

// Wrong: the Promise starts now, before a Run starts the Task.
const promise = createPromiseReturningResult();
const task: Task<ResultValue, MyError> = () => promise;

expect(promiseStarted).toBe(true);
expectTypeOf(task).toEqualTypeOf<Task<ResultValue, MyError>>();