Automattic/mongoose · error · CastError

Cast to $text failed for value

Error message

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

What it means

castTextSearch handles the top-level `$text` filter key (lib/cast.js routes `filter.$text` here) and throws CastError '$text' when the value is null or not an object. $text requires an object form — `{ $search: 'term', $language: 'en', $caseSensitive: false, $diacriticSensitive: false }` — and each present sub-option is then cast to its type. Passing the search string bare ('term') or forgetting the object wrapper triggers this before the query ever reaches MongoDB.

Solutions

  1. Always use the object form: `Model.find({ $text: { $search: term } })`
  2. Default-construct the object when the term may be absent: `const q = term ? { $text: { $search: term } } : {}`
  3. Ensure a text index exists (`schema.index({ body: 'text' })`) — without it a valid $text query fails server-side with a different error
  4. Validate search input is a non-empty string before composing the filter

Example fix

// before
const results = await Model.find({ $text: req.query.q }); // bare string

// after
const q = String(req.query.q ?? '').trim();
if (!q) return [];
const results = await Model.find({ $text: { $search: q } });
Defensive patterns

Strategy: validation

Validate before calling

function toTextFilter(term) {
  const q = String(term ?? '').trim();
  if (!q) return {}; // no text search
  return { $text: { $search: q } }; // always object form
}

Type guard

function isTextFilter(v) {
  return v != null && typeof v === 'object' && typeof v.$search === 'string';
}

Try / catch

try { await Model.find({ $text: value }); } catch (err) { if (err.name === 'CastError' && err.kind === '$text') { return badRequest('$text requires { $search: string }'); } throw err; }

Prevention

When it happens

Trigger: `Model.find({ $text: 'hello' })` (bare string), `Model.find({ $text: null })`, or `{ $text: { $search: 123 } }` (inner options then fail their own casts via castString/castBoolean on `path.$search` etc.). Note $text is a top-level filter key, never placed under a field path.

Common situations: Wrapping a search term directly instead of `{ $search: ... }`; query-builder code that short-circuits empty searches to the raw string; missing text index is a *different* (server-side) error — this one is purely shape validation on the filter.

Related errors


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

Appendix: source

Thrown at lib/schema/operators/text.js:20

const CastError = require('../../error/cast');
const castBoolean = require('../../cast/boolean');
const castString = require('../../cast/string');

/**
 * Casts val to an object suitable for `$text`. Throws an error if the object
 * can't be casted.
 *
 * @param {any} val value to cast
 * @param {string} [path] path to associate with any errors that occurred
 * @return {object} casted object
 * @see https://www.mongodb.com/docs/manual/reference/operator/query/text/
 * @api private
 */

module.exports = function castTextSearch(val, path) {
  if (val == null || typeof val !== 'object') {
    throw new CastError('$text', val, path);
  }

  if (val.$search != null) {
    val.$search = castString(val.$search, path + '.$search');
  }
  if (val.$language != null) {
    val.$language = castString(val.$language, path + '.$language');
  }
  if (val.$caseSensitive != null) {
    val.$caseSensitive = castBoolean(val.$caseSensitive,
      path + '.$caseSensitive');
  }
  if (val.$diacriticSensitive != null) {
    val.$diacriticSensitive = castBoolean(val.$diacriticSensitive,
      path + '.$diacriticSensitive');
  }

  return val;

View on GitHub (pinned to 49cdab0136)