Skip to content

auto

auto runs named asynchronous tasks in dependency order. Use it when each task may depend on the results of other tasks and independent tasks should still run concurrently.

Best for dependency graphs Use auto for workflows such as loading configuration, fetching user data, preparing dependent requests, and combining the final result after all prerequisites are available.

Root package:

import { auto } from "@vgerbot/async";

Module subpath:

import { auto } from "@vgerbot/async/control-flow";

Leaf subpath:

import { auto } from "@vgerbot/async/control-flow/auto";

Each task is declared by name. A task can be either a function with no dependencies or a tuple of [dependencies, task].

import { auto } from "@vgerbot/async";
const handle = auto<{
config: { baseUrl: string };
user: { id: number; name: string };
posts: Array<{ id: number; title: string }>;
summary: string;
}>(
{
config: async () => ({ baseUrl: "https://api.example.com" }),
user: [
["config"],
async ({ config }, token) => {
await token.sleep(100);
const response = await token.wrap(
fetch(`${config.baseUrl}/users/1`),
);
return response.json();
},
],
posts: [
["config", "user"],
async ({ config, user }, token) => {
const response = await token.wrap(
fetch(`${config.baseUrl}/users/${user.id}/posts`),
);
return response.json();
},
],
summary: [
["user", "posts"],
async ({ user, posts }) => {
return `${user.name} has ${posts.length} posts`;
},
],
},
{ concurrency: 2 },
);
const result = await handle;
console.log(result.summary);
  • Dependent async work: Model workflows where each step has named prerequisites and consumes their results.
  • Parallel where possible: Independent tasks are scheduled concurrently, while dependent tasks wait for their inputs.
  • Typed result maps: Describe the final result shape once and get strongly typed dependency inputs.
  • Cancelable workflows: Every task receives a CancellableToken, and the returned handle can stop the graph.
function auto<TResults extends Record<string, unknown>>(
tasks: AutoTasks<TResults>,
options: AutoResolveOptions<TResults>,
): CancellableHandle<AutoResolveResult<TResults>>;
function auto<TResults extends Record<string, unknown>>(
tasks: AutoTasks<TResults>,
options?: AutoRejectOptions<TResults>,
): CancellableHandle<AutoResult<TResults>>;

A map where each key is a task name and each value describes how that task should run.

type AutoTasks<TResults extends Record<string, unknown>> = {
[K in keyof TResults]: AutoTask<TResults, TResults[K]>;
};

A task can be written in two forms:

const taskWithoutDependencies = async (results, token) => value;
const taskWithDependencies = [
["dependencyA", "dependencyB"],
async (results, token) => value,
];

The results object only contains completed dependencies. When dependencies are declared with tuple syntax, TypeScript narrows results to the named dependencies.

Option Type Default Description
concurrency number Infinity Maximum number of active tasks. Values below 1 are normalized to 1.
errorMode "reject" | "resolve" "reject" Controls whether task failures reject the handle or resolve with partial results and an error.
name string "auto" Name used in cancellation and task error labels.
signal AbortSignal undefined External signal linked to the workflow.
timeout number undefined Cancels the workflow after the specified milliseconds.
fallback value or function undefined Fallback value used by the underlying cancellable task when it rejects.
retry RetryOptions undefined Retry configuration applied to the whole auto workflow.
onCancel function undefined Called when the workflow is cancelled.
onRetry function undefined Called before a retry attempt.

auto returns a CancellableHandle.

const handle = auto(tasks);
await handle;
handle.cancel("No longer needed");
handle.isCancelled();
handle.signal;

In default reject mode, handle resolves to the complete result map.

const results = await auto(tasks);

In resolve mode, handle always resolves to an object containing partial results and an optional AutoExecutionError.

const { results, error } = await auto(tasks, {
errorMode: "resolve",
});

auto repeatedly schedules ready tasks until the graph completes or fails.

  1. Parse task declarations.
  2. Validate that every dependency points to a known task.
  3. Start tasks whose dependencies are already completed.
  4. Respect concurrency while scheduling more ready tasks.
  5. Store each task result by its task name.
  6. Stop scheduling new tasks after the first non-cancellation error.
  7. Wait for already active tasks to settle before returning or throwing.
const handle = auto(
{
a: async () => "a",
b: async () => "b",
c: [["a", "b"], async ({ a, b }) => `${a}${b}`],
},
{ concurrency: 2 },
);
console.log(await handle);
// { a: "a", b: "b", c: "ab" }

Reject mode is the default. If a task fails, handle rejects with AutoExecutionError.

import { AutoExecutionError, auto } from "@vgerbot/async";
const handle = auto<{
user: { id: number };
posts: unknown[];
}>({
user: async () => ({ id: 1 }),
posts: [
["user"],
async () => {
throw new Error("Posts service unavailable");
},
],
});
try {
await handle;
} catch (error) {
if (error instanceof AutoExecutionError) {
console.log(error.taskName);
console.log(error.cause);
console.log(error.partialResults);
}
}

Resolve mode is useful when partial results are valuable and failure should be handled as data.

const { results, error } = await auto<{
profile: { id: number };
recommendations: string[];
}>(
{
profile: async () => ({ id: 1 }),
recommendations: [
["profile"],
async () => {
throw new Error("Recommendation service failed");
},
],
},
{ errorMode: "resolve" },
);
if (error) {
console.log(results.profile);
console.warn(error.message);
}

Every task receives a CancellableToken. Use it to make sleeps, fetches, nested handles, and explicit checks react to cancellation.

const handle = auto(
{
slowTask: async (_results, token) => {
await token.sleep(5_000);
token.throwIfCancelled();
return "done";
},
},
{ name: "loadDashboard" },
);
setTimeout(() => {
handle.cancel("User navigated away");
}, 100);
await handle;

Cancellation rejects with CancelError When cancellation occurs, auto propagates the cancellation error instead of wrapping it in AutoExecutionError.

auto validates dependency names before running tasks.

await auto({
user: [["missingConfig"], async () => ({ id: 1 })],
});
// Error: auto task "user" depends on unknown task "missingConfig"

If tasks cannot be resolved because of a cycle, the promise rejects with a cycle error.

await auto({
a: [["b"], async () => 1],
b: [["a"], async () => 2],
});
// Error: auto cannot resolve dependencies (possible cycle): a[b]; b[a]

Use a result map type to make dependency inputs precise.

type WorkflowResults = {
config: { apiUrl: string };
session: { token: string };
dashboard: { widgets: string[] };
};
const handle = auto<WorkflowResults>({
config: async () => ({ apiUrl: "/api" }),
session: [["config"], async ({ config }) => ({ token: config.apiUrl })],
dashboard: [
["config", "session"],
async ({ config, session }) => ({
widgets: [`${config.apiUrl}:${session.token}`],
}),
],
});

Use as const when TypeScript widens dependency arrays too much.

const handle = auto<WorkflowResults>({
config: [[], async () => ({ apiUrl: "/api" })] as const,
session: [["config"], async ({ config }) => ({ token: config.apiUrl })] as const,
dashboard: [
["config", "session"],
async ({ config, session }) => ({
widgets: [`${config.apiUrl}:${session.token}`],
}),
] as const,
});
  • parallel runs independent tasks concurrently.
  • series runs tasks one after another.
  • waterfall passes each task result into the next task.
  • queue manages a long-lived worker queue.
  • cancellable creates a cancellable handle directly.