Hooks are registered on the schema, before the model is compiled, and come in families that differ in what this is. Getting that wrong is the classic Mongoose 243,355 bug.

const schema = new Schema({ title: String, slug: String, updatedBy: String });
schema.pre('save', function () {
log(`pre save this is a ${this.constructor.modelName}`);
this.slug = this.title.toLowerCase().replaceAll(' ', '-');
});
schema.post('save', function (doc) { log(`post save slug=${doc.slug}`); });
schema.pre('findOneAndUpdate', function () {
log(`pre findOne... this is a ${this.constructor.name}, ` +
`filter=${JSON.stringify(this.getFilter())}`);
this.set({ updatedBy: 'hook' });
});
schema.post('findOneAndUpdate', function (r) {
log(`post findOne... title=${r.title} slug=${r.slug} updatedBy=${r.updatedBy}`);
});
await new Post({ title: 'The Cyberiad' }).save();
await Post.findOneAndUpdate({ title: 'The Cyberiad' }, { title: 'Peace on Earth' },
{ returnDocument: 'after' });pre save this is a Post
post save slug=the-cyberiad
pre findOne... this is a Query, filter={"title":"The Cyberiad"}
post findOne... title=Peace on Earth slug=the-cyberiad updatedBy=hookRead the last line twice. The title is now "Peace on Earth" and the slug still says the-cyberiad, because pre('save') never ran: a query update does not load a document, so there is nothing for a document hook to act on. Data drifts out of sync exactly this quietly. Either derive the field in a query hook too — schema.pre(['findOneAndUpdate', 'updateOne'], ...) takes an array, and /^find/ matches a whole group — or forbid the query path for that model.
Inside query middleware this is the Query: use this.getFilter(), this.getUpdate() and this.set(). Inside document middleware this is the document. Either way a hook must be a function, never an arrow, which would capture the module's this. Two more families exist: aggregate middleware binds this to the Aggregate, and model middleware covers insertMany and bulkWrite. pre hooks run serially in registration order, and a thrown error aborts the operation. Register them before mongoose.model() compiles the schema; afterwards they are silently ignored, which is the second-most-common Mongoose bug and the reason a plugin (Plugins) is the right home for shared hooks.