Automattic/mongoose · warning · TypeError

Invalid graphLookup() argument. Must be an object.

Error message

Invalid graphLookup() argument. Must be an object.

What it means

Mongoose prints this warning at require time when it detects Jest fake timers: with mocked timers, setTimeout has a .clock object with a Date function (jest.useFakeTimers() installs it). Mongoose and the MongoDB driver rely on real timers for connection timeouts, heartbeats, and immediate callbacks, so fake timers can hang or slow every DB operation in a test. The check is typeof setTimeout.clock?.Date === 'function'; SUPPRESS_JEST_WARNINGS hides it.

Source

Thrown at lib/aggregate.js:595

 *
 * Note that graphLookup can only consume at most 100MB of memory, and does not allow disk use even if `{ allowDiskUse: true }` is specified.
 *
 * #### Example:
 *
 *      // Suppose we have a collection of courses, where a document might look like `{ _id: 0, name: 'Calculus', prerequisite: 'Trigonometry'}` and `{ _id: 0, name: 'Trigonometry', prerequisite: 'Algebra' }`
 *      aggregate.graphLookup({ from: 'courses', startWith: '$prerequisite', connectFromField: 'prerequisite', connectToField: 'name', as: 'prerequisites', maxDepth: 3 }) // this will recursively search the 'courses' collection up to 3 prerequisites
 *
 * @see $graphLookup https://www.mongodb.com/docs/manual/reference/operator/aggregation/graphLookup/#pipe._S_graphLookup
 * @param {object} options to $graphLookup as described in the above link
 * @return {Aggregate}
 * @api public
 */

Aggregate.prototype.graphLookup = function(options) {
  const cloneOptions = {};
  if (options) {
    if (!utils.isObject(options)) {
      throw new TypeError('Invalid graphLookup() argument. Must be an object.');
    }

    utils.mergeClone(cloneOptions, options);
    const startWith = cloneOptions.startWith;

    if (startWith && typeof startWith === 'string') {
      cloneOptions.startWith = cloneOptions.startWith.startsWith('$') ?
        cloneOptions.startWith :
        '$' + cloneOptions.startWith;
    }

  }
  return this.append({ $graphLookup: cloneOptions });
};

/**
 * Appends new custom $sample operator to this aggregate pipeline.
 *

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Scope fake timers to the tests that need them and restore afterwards: afterEach(() => jest.useRealTimers()).
  2. Move DB-dependent tests into files that never enable fake timers (often paired with testEnvironment: 'node').
  3. If you must keep fake timers, configure them to leave some real timers: jest.useFakeTimers({ doNotFake: ['nextTick', 'setImmediate', 'setTimeout'] }) as appropriate.
  4. Use jest.useFakeTimers({ advanceTimers: false }) plus explicit jest.advanceTimersByTime() only around the scheduling code under test, not the connection logic.

Example fix

// before
jest.useFakeTimers();
require('mongoose'); // warning + hanging queries

// after
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
// DB tests live in files that never fake timers
Defensive patterns

Strategy: validation

Validate before calling

// jest.setup.js — keep real timers for anything touching the DB
beforeEach(() => {
  jest.useRealTimers();
});

Prevention

When it happens

Trigger: Calling jest.useFakeTimers() (legacy or modern) before or after requiring mongoose in the same test file; global fake-timer setup files in jest.setup.js applied to every test; waitFor/advanceTimersByTime loops around code that opens a MongoDB connection.

Common situations: Unit tests that fake timers for debouncing/scheduling code in the same file as model tests; setupFilesAfterEach enabling fake timers globally; CI flakiness where connections time out because timer callbacks never fire; migration to modern fake timers which also set setTimeout.clock.

Related errors


AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21). Data as JSON: /api/errors/278c16ddfd310b76. Report an issue: GitHub.