Automattic/mongoose · error · MongooseError

Invalid project() argument. Must be string or object

Error message

Invalid project() argument. Must be string or object

What it means

utils.errorToPOJO() converts an Error instance into a plain object by copying its own enumerable and non-enumerable property names. Because the whole point is to serialize error metadata (stack, message, code) for transport or aggregation, it refuses anything that is not an instanceof Error and throws a plain Error with this message. Mongoose uses it internally when aggregating errors (for example in Model.create() with aggregateErrors and in bulk write error reporting).

Source

Thrown at lib/aggregate.js:264

  const fields = {};

  if (typeof arg === 'object' && !Array.isArray(arg)) {
    Object.keys(arg).forEach(function(field) {
      fields[field] = arg[field];
    });
  } else if (arguments.length === 1 && typeof arg === 'string') {
    arg.split(/\s+/).forEach(function(field) {
      if (!field) {
        return;
      }
      const include = field[0] === '-' ? 0 : 1;
      if (include === 0) {
        field = field.substring(1);
      }
      fields[field] = include;
    });
  } else {
    throw new MongooseError('Invalid project() argument. Must be string or object');
  }

  return this.append({ $project: fields });
};

/**
 * Appends a new custom $group operator to this aggregate pipeline.
 *
 * #### Example:
 *
 *     aggregate.group({ _id: "$department" });
 *
 * @see $group https://www.mongodb.com/docs/manual/reference/aggregation/group/
 * @method group
 * @memberOf Aggregate
 * @instance
 * @param {object} arg $group operator contents
 * @return {Aggregate}

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Pass an actual Error instance: errorToPOJO(new Error('...')).
  2. If the value may be a plain object or string, re-wrap it first: typeof x === 'string' || !(x instanceof Error) ? new Error(x) : x.
  3. For cross-realm errors (vm, Jest fake modules), construct errors in the main realm, or duck-type check (x && x.stack && x.message) and wrap.

Example fix

// before
utils.errorToPOJO({ message: 'boom', code: 42 }); // throws

// after
const err = maybeErr instanceof Error ? maybeErr : new Error(String(maybeErr?.message ?? maybeErr));
const pojo = utils.errorToPOJO(err);
Defensive patterns

Strategy: type-guard

Validate before calling

function toErrorPojoSafe(value) {
  const err = value instanceof Error ? value : new Error(String(value?.message ?? value));
  return utils.errorToPOJO(err);
}

Type guard

const isErrorLike = (x) =>
  x instanceof Error || (x != null && typeof x === 'object' && typeof x.stack === 'string' && typeof x.message === 'string');

Try / catch

try {
  return utils.errorToPOJO(maybeError);
} catch (err) {
  // re-wrap non-Error input and retry once
  return utils.errorToPOJO(new Error(String(maybeError?.message ?? maybeError)));
}

Prevention

When it happens

Trigger: Calling utils.errorToPOJO('string') or errorToPOJO({ message: 'x' }) directly; passing a mongoose error-like plain object or an error from another realm/VM (e.g. errors created inside a Jest module registry or vm context fail instanceof Error); libraries that wrap errors as POJOs before Mongoose aggregates them.

Common situations: Cross-realm errors in Jest or worker_threads where instanceof fails; user code that normalizes errors to plain objects and then passes them back into an API expecting Error instances; retry wrappers that store err.toJSON() and later feed it to error aggregation.

Related errors


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