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

  1. Break the cycle by removing or restructuring one dependency edge
  2. Draw the task graph and identify the loop before running
  3. Split a mutually dependent task into two ordered tasks
  4. 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

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


AI-assisted analysis of caolan/async@13dfaf13f3 (2026-08-28). Data as JSON: /api/errors/edc11e1d57a6ac63. Report an issue: GitHub.