{"record":{"id":"29a99908c636f904","repo":"Automattic/mongoose","slug":"transform-function-must-be-synchronous-but-the","errorCode":null,"errorMessage":"`transform` function must be synchronous, but the transform on path `${path}` returned a promise.","messagePattern":"`transform` function must be synchronous, but the transform on path `(.+?)` returned a promise\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"lib/document.js","lineNumber":4629,"sourceCode":"        continue;\n      }\n      const vals = [].concat(val);\n      for (let i = 0; i < vals.length; ++i) {\n        const transformedValue = embeddedSchemaTypeTransformFunction.call(self, vals[i]);\n        vals[i] = transformedValue;\n        throwErrorIfPromise(path, transformedValue);\n      }\n\n      json[path] = vals;\n    }\n  }\n\n  return json;\n}\n\nfunction throwErrorIfPromise(path, transformedValue) {\n  if (isPromise(transformedValue)) {\n    throw new Error('`transform` function must be synchronous, but the transform on path `' + path + '` returned a promise.');\n  }\n}\n\n/*!\n * ignore\n */\n\nfunction omitDeselectedFields(self, json) {\n  const schema = self.$__schema;\n  const paths = Object.keys(schema.paths || {});\n  const cur = self._doc;\n\n  if (!cur) {\n    return json;\n  }\n\n  let selected = self.$__.selected;\n  if (selected === void 0) {","sourceCodeStart":4611,"sourceCodeEnd":4647,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/document.js#L4611-L4647","documentation":"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.","triggerScenarios":"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.","commonSituations":"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()).","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"],"exampleFix":"// before\nconst schema = new Schema({ ssn: { type: String, transform: async v => decrypt(v) } });\nconst json = doc.toJSON(); // throws: transform returned a promise\n\n// after\nconst ssn = await decrypt(doc.ssn);\nconst json = doc.toJSON(); // with transform: v => v, or set the pre-decrypted value on the doc first","handlingStrategy":"try-catch","validationCode":"// Smoke-test transforms at startup so a promise-returning transform fails loudly in CI\nfor (const [path, schemaType] of Object.entries(MyModel.schema.paths)) {\n  const t = schemaType.options?.transform;\n  if (typeof t === 'function' && t.constructor?.name === 'AsyncFunction') {\n    throw new Error(`Transform on path ${path} must be synchronous`);\n  }\n}","typeGuard":"const isSyncFunction = (fn) => typeof fn === 'function' && fn.constructor?.name !== 'AsyncFunction' && !(fn instanceof Promise);","tryCatchPattern":"try {\n  const json = doc.toJSON();\n} catch (err) {\n  if (/transform on path .* returned a promise/.test(err.message)) {\n    // a path transform is async; precompute its result before serializing\n  } else { throw err; }\n}","preventionTips":["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"],"tags":["mongoose","tojson","transform","async","serialization"],"backgroundTag":"async-transform-not-allowed","analyzedSha":"49cdab01366679723b487ecb754b38570f783289","analyzedAt":"2026-08-21T22:54:00.882Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}