Automattic/mongoose · error · Error

`transform` function must be synchronous, but the transform

Error message

`transform` function must be synchronous, but the transform on path `${path}` returned a promise.

What it means

toJSON()/toObject() serialization is fully synchronous, so per-path `transform` functions must be too. If a path's transform option (checked via throwErrorIfPromise while building the JSON) returns a promise, Mongoose throws instead of producing a document with pending values embedded.

Source

Thrown at lib/document.js:4629

        continue;
      }
      const vals = [].concat(val);
      for (let i = 0; i < vals.length; ++i) {
        const transformedValue = embeddedSchemaTypeTransformFunction.call(self, vals[i]);
        vals[i] = transformedValue;
        throwErrorIfPromise(path, transformedValue);
      }

      json[path] = vals;
    }
  }

  return json;
}

function throwErrorIfPromise(path, transformedValue) {
  if (isPromise(transformedValue)) {
    throw new Error('`transform` function must be synchronous, but the transform on path `' + path + '` returned a promise.');
  }
}

/*!
 * ignore
 */

function omitDeselectedFields(self, json) {
  const schema = self.$__schema;
  const paths = Object.keys(schema.paths || {});
  const cur = self._doc;

  if (!cur) {
    return json;
  }

  let selected = self.$__.selected;
  if (selected === void 0) {

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Make the transform synchronous: precompute the async result and read it inside the transform
  2. Move async work out of the schema: decrypt or map in application code before serializing
  3. Do the async shaping manually after toJSON() by mapping over the plain object

Example fix

// before
const schema = new Schema({ ssn: { type: String, transform: async v => decrypt(v) } });
const json = doc.toJSON(); // throws: transform returned a promise

// after
const ssn = await decrypt(doc.ssn);
const json = doc.toJSON(); // with transform: v => v, or set the pre-decrypted value on the doc first
Defensive patterns

Strategy: try-catch

Validate before calling

// Smoke-test transforms at startup so a promise-returning transform fails loudly in CI
for (const [path, schemaType] of Object.entries(MyModel.schema.paths)) {
  const t = schemaType.options?.transform;
  if (typeof t === 'function' && t.constructor?.name === 'AsyncFunction') {
    throw new Error(`Transform on path ${path} must be synchronous`);
  }
}

Type guard

const isSyncFunction = (fn) => typeof fn === 'function' && fn.constructor?.name !== 'AsyncFunction' && !(fn instanceof Promise);

Try / catch

try {
  const json = doc.toJSON();
} catch (err) {
  if (/transform on path .* returned a promise/.test(err.message)) {
    // a path transform is async; precompute its result before serializing
  } else { throw err; }
}

Prevention

When it happens

Trigger: Declaring `ssn: { type: String, transform: async v => decrypt(v) }` and then calling doc.toJSON()/toObject(); refactors that turned a previously sync transform (decrypt, id mapping, formatting with lookups) into an async one.

Common situations: Adding decryption or external-service lookups inside transforms; converting shared utility helpers to async without noticing schema transforms call them; API response builders relying on res.json(doc.toJSON()).

Related errors


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