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
- Make the transform synchronous: precompute the async result and read it inside the transform
- Move async work out of the schema: decrypt or map in application code before serializing
- 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
- Keep path transforms synchronous; do async work (decrypt, lookups) before toJSON()
- Lint schema definitions to reject async functions in transform options
- Add a unit test that calls toJSON() on a fully populated document
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
- Invalid project() argument. Must be string or object
- a circular reference in the update value, updateValue: ${uti
- Arguments must be aggregate pipeline operators
- Invalid addFields() argument. Must be an object
- Aggregate `near()` must be called with non-nullish argument
AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21).
Data as JSON: /api/errors/29a99908c636f904.
Report an issue: GitHub.