Automattic/mongoose · error · CastError

Cast to ObjectId failed for value "${value}" (type ${valueTy

Error message

Cast to ObjectId failed for value "${value}" (type ${valueType}) at path "${path}"

What it means

SchemaObjectId.cast wraps the ObjectId caster (lib/cast/objectid.js), which passes through real ObjectIds, plucks `value._id` when present, and otherwise calls `new ObjectId(value.toString())`. That bson constructor throws for any string that is not 24 hex characters (or 12 bytes), and mongoose wraps the failure as CastError 'ObjectId' — the wrapped form of bson's 'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'.

Source

Thrown at lib/schema/objectId.js:252

    if (value == null || utils.isNonBuiltinObject(value)) {
      return this._castRef(value, doc, init, options);
    }
  }

  let castObjectId;
  if (typeof this._castFunction === 'function') {
    castObjectId = this._castFunction;
  } else if (typeof this.constructor.cast === 'function') {
    castObjectId = this.constructor.cast();
  } else {
    castObjectId = SchemaObjectId.cast();
  }

  try {
    return castObjectId(value);
  } catch (error) {
    throw new CastError('ObjectId', value, this.path, error, this);
  }
};

/*!
 * ignore
 */

function handleSingle(val) {
  return this.cast(val);
}

const $conditionalHandlers = {
  ...SchemaType.prototype.$conditionalHandlers,
  $gt: handleSingle,
  $gte: handleSingle,
  $lt: handleSingle,
  $lte: handleSingle
};

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Validate first: `if (!mongoose.isValidObjectId(id)) return 400` — check shape before touching the model
  2. Convert explicitly at the boundary inside try/catch: `new mongoose.Types.ObjectId(id)` and reject on throw
  3. If you accept non-ObjectIds (UUIDs etc.), change the path type to String
  4. In bulk $in filters, filter the array first: `ids.filter(mongoose.isValidObjectId)`

Example fix

// before
const user = await User.findById(req.params.id); // 'abc' → CastError

// after
if (!mongoose.isValidObjectId(req.params.id)) {
  return res.status(400).json({ error: 'invalid id' });
}
const user = await User.findById(req.params.id);
Defensive patterns

Strategy: type-guard

Validate before calling

if (!mongoose.isValidObjectId(id)) {
  throw Object.assign(new Error('invalid id'), { status: 400 });
}
const doc = await Model.findById(id);

Type guard

function isObjectIdLike(v) {
  return v == null || v instanceof mongoose.Types.ObjectId ||
    (typeof v === 'string' && /^[a-f0-9]{24}$/i.test(v));
}

Try / catch

try { doc.owner = id; } catch (err) { if (err.name === 'CastError' && err.kind === 'ObjectId') { return badRequest(`invalid ObjectId for ${err.path}`); } throw err; }

Prevention

When it happens

Trigger: `doc.owner = 'abc'` (wrong length), `doc._id = '507f1f77bcf86cd7994d90'` (23 chars), `{ owner: { $in: ['xyz', ...] } }`, assigning UUIDs or arbitrary slugs to an ObjectId path, or an object whose toString() yields a non-hex string.

Common situations: Route params (`/users/:id`) receiving garbage or empty strings without validation; ids from external systems (UUID, ULID, ints) assigned to ObjectId fields; truncated ids from logs; case-sensitive hex issues (uppercase hex actually works — length is the usual culprit); populated docs re-serialized to strings with quotes.

Related errors


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