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).

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.

Appendix: 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)