What it means
An ObjectId is MongoDB’s default _id: 12 bytes, written as 24 hexadecimal characters such as
65f1c2e9a1b2c3d4e5f6a7b8. When a schema path is an ObjectId (_id always is unless you declare
it otherwise, and so is every ref field), Mongoose converts whatever you pass into one before it
builds the query or saves the document. Mongoose calls that casting. If the value can’t become an
ObjectId, Mongoose throws a CastError and nothing is sent to MongoDB.
The message names the three things you need:
for value "abc" (type string): the value it got, and its JavaScript type.at path "_id": the field.for model "User": the model you queried.
The error’s reason holds the underlying failure from the bson library,
input must be a 24 character hex string. When you
save a document instead of querying, the CastError is wrapped in a ValidationError:
Order validation failed: user: Cast to ObjectId failed for value … because of "BSONError".
Common causes
- A route parameter that isn’t an id.
/users/:idreceivesme,new, the literal:id, orundefinedfrom a front end that built the URL from a variable that wasn’t set yet. - Route order. In Express,
/users/:iddeclared before/users/newcatchesnewas an id. - A truncated or padded id: 23 characters copied instead of 24, or a leading or trailing space or newline.
- The wrong value in a reference field: an email address, a name or a whole object where the
schema expects the referenced document’s
_id. - Ids that aren’t ObjectIds. The collection uses string or numeric
_ids (slugs, UUIDs, imported keys) but the schema still has the default ObjectId_id.
How to fix it
Find the value and where it came from
Log the error’s fields rather than only its message:
catch (err) {
if (err.name === "CastError") console.log(err.path, err.kind, JSON.stringify(err.value))
}
JSON.stringify shows spaces and newlines that a plain log hides. Then trace the value back to
the request, form or variable that produced it.
Check the id before you query
Answer a bad id with 400 and a missing document with 404, instead of letting the CastError become a 500:
const mongoose = require("mongoose")
app.get("/users/:id", async (req, res) => {
if (!mongoose.isObjectIdOrHexString(req.params.id)) {
return res.status(400).json({ error: "Invalid user id" })
}
const user = await User.findById(req.params.id)
if (!user) return res.status(404).end()
res.json(user)
})
isObjectIdOrHexString accepts only an ObjectId or a 24-character hex string.
mongoose.isValidObjectId is looser: it also returns true for a number such as 123, because
an ObjectId can be built from an integer.
Put fixed routes before parameter routes
app.get("/users/new", showNewUserForm)
app.get("/users/me", showCurrentUser)
app.get("/users/:id", showUser)
Store the referenced document’s _id
Look the document up first, then store its _id:
const user = await User.findOne({ email: "ada@example.com" })
await Order.create({ user: user._id, total: 5 })
Declare the _id you really use
If your ids are strings or numbers, say so in the schema, and Mongoose stops casting them:
const Page = mongoose.model("Page", new mongoose.Schema({ _id: String, title: String }))
await Page.findById("about-us")
To see which type the stored ids have, ask the server:
db.pages.aggregate([{ $group: { _id: { $type: "$_id" }, n: { $sum: 1 } } }]).
Handle it once, in your error middleware
app.use((err, req, res, next) => {
if (err.name === "CastError") return res.status(400).json({ error: `Invalid ${err.path}` })
next(err)
})
Reproduce it
Mongoose 8.23.0 (with the Node.js driver 6.16.0) against MongoDB 8.0.32, with a User model and
an Order model whose user field is an ObjectId reference:
await User.findById("abc")
CastError: Cast to ObjectId failed for value "abc" (type string) at path "_id" for model "User"
Its reason was BSONError: input must be a 24 character hex string, 12 byte Uint8Array, or an integer, kind was ObjectId and path _id. The values "undefined", "me", ":id", a
23-character id and an id with a leading space all failed the same way, as did
User.findOne({ _id: "new" }). An object failed as (type Object):
CastError: Cast to ObjectId failed for value "{ name: 'x' }" (type Object) at path "_id" for model "User"
Saving an email address into the reference field:
await Order.create({ user: "ada@example.com", total: 5 })
ValidationError: Order validation failed: user: Cast to ObjectId failed for value "ada@example.com" (type string) at path "user" because of "BSONError"
Order.find({ user: { $in: ["x", user._id] } }) failed at path user. A well-formed id that
matches no document returned null without an error, and so did findById(undefined).
isValidObjectId returned false for "abc" and for a 12-character string, and true for
123; isObjectIdOrHexString(123) returned false. With _id: String in the schema,
findById("about-us") found its document.
In Inlet
Inlet shows MongoDB collections as tables and each document in the inspector, so you can see
whether _id and reference fields hold ObjectIds or strings. Queries use mongosh syntax, so
db.orders.find({ user: ObjectId("65f1c2e9a1b2c3d4e5f6a7b8") }) checks what a reference points at.