caolan/async · error · Error
async.auto cannot execute tasks due to a recursive dependenc
Error message
async.auto cannot execute tasks due to a recursive dependency
What it means
Before executing, async.auto performs a topological-sort pass (checkForDeadlocks) counting how many tasks become 'ready'. If the count never reaches the total number of tasks, the dependency graph contains a cycle and auto throws instead of hanging. No task in a cyclic graph can ever start.
Source
Thrown at lib/auto.js:303
function checkForDeadlocks() {
// Kahn's algorithm
// https://en.wikipedia.org/wiki/Topological_sorting#Kahn.27s_algorithm
// http://connalle.blogspot.com/2013/10/topological-sortingkahn-algorithm.html
var currentTask;
var counter = 0;
while (readyToCheck.length) {
currentTask = readyToCheck.pop();
counter++;
getDependents(currentTask).forEach(dependent => {
if (--uncheckedDependencies[dependent] === 0) {
readyToCheck.push(dependent);
}
});
}
if (counter !== numTasks) {
throw new Error(
'async.auto cannot execute tasks due to a recursive dependency'
);
}
}
function getDependents(taskName) {
var result = [];
Object.keys(tasks).forEach(key => {
const task = tasks[key]
if (Array.isArray(task) && task.indexOf(taskName) >= 0) {
result.push(key);
}
});
return result;
}
return callback[PROMISE_SYMBOL]
}View on GitHub (pinned to 13dfaf13f3)
Solutions
- Break the cycle by removing or restructuring one dependency edge
- Draw the task graph and identify the loop before running
- Split a mutually dependent task into two ordered tasks
- If the data truly flows both ways, merge the tasks into one
Example fix
// before
async.auto({
a: ['b', stepA],
b: ['a', stepB]
});
// after
async.auto({
a: [stepA],
b: ['a', stepB]
}); Defensive patterns
Strategy: validation
Validate before calling
function hasCycle(tasks) {
const state = {};
const deps = k => (Array.isArray(tasks[k]) ? tasks[k][0] : []);
let cyclic = false;
function visit(k, stack) {
if (stack.has(k)) { cyclic = true; return; }
if (state[k]) return;
state[k] = 1;
deps(k).forEach(d => visit(d, new Set([...stack, k])));
state[k] = 2;
}
Object.keys(tasks).forEach(k => visit(k, new Set()));
return cyclic;
} Try / catch
try {
await async.auto(tasks);
} catch (err) {
if (String(err.message).includes('recursive dependency')) {
console.error('Circular dependency in task graph');
} else throw err;
} Prevention
- Design task graphs as a DAG and document the ordering
- Add a cycle-detection test for dynamically generated graphs
- Avoid mutual dependencies; prefer splitting or merging tasks
When it happens
Trigger: Defining tasks whose dependencies form a loop, e.g. async.auto({a: ['b', fnA], b: ['a', fnB]}). Also with longer cycles a -> b -> c -> a, or a task listing itself as a dependency: {a: ['a', fn]}.
Common situations: Refactoring a task graph where two tasks accidentally started depending on each other, merging generated dependency lists that reference each other, dynamic graphs built from user config containing circular references.
Related errors
- async.auto task `${key}` has a non-existent dependency `${de
- could not parse args in autoInject Source: ${src}
- autoInject task functions require explicit parameters.
- arity is undefined
- concurrency limit cannot be less than 1
AI-assisted analysis of caolan/async@13dfaf13f3 (2026-08-28).
Data as JSON: /api/errors/edc11e1d57a6ac63.
Report an issue: GitHub.