The Error
BSONTypeError: Argument passed in must be a string of 12 bytes or a string of 24 hex characters or an integer
This fires when you pass something invalid to new ObjectId(). The usual suspects: undefined, an empty string, or a mangled ID that arrived from user input or a bad URL param.
Why This Happens
MongoDB ObjectIds are exactly 24 hex characters (e.g. 507f1f77bcf86cd799439011) or 12 raw bytes. BSON is strict about this โ pass anything else and it throws immediately, no partial matches.
Most common triggers:
- Route param is
undefinedbecause the param name in the route doesn't match what you're reading - Client sends a garbage string or a short ID instead of a real ObjectId
- An ObjectId string got truncated in transit โ even one missing character breaks it
- Importing
ObjectID(uppercase D, mongodb@3 style) in a mongodb@4+ project โ silently gives youundefined - Passing a document's
_idthat's already an ObjectId instance back intonew ObjectId()โ redundant, though usually harmless
Step-by-Step Fix
1. Always validate before creating ObjectId
BSON includes ObjectId.isValid() for exactly this. Call it every time an ID comes from outside your code โ route params, request bodies, query strings, all of it.
const { ObjectId } = require('mongodb'); // or from 'bson'
// DON'T โ blows up if id is undefined or malformed
const id = new ObjectId(req.params.id);
// DO โ validate first
const rawId = req.params.id;
if (!ObjectId.isValid(rawId)) {
return res.status(400).json({ error: 'Invalid ID format' });
}
const id = new ObjectId(rawId);
2. Check the import โ ObjectId vs ObjectID
In mongodb@3, the export was ObjectID (capital D). Starting with mongodb@4, it changed to ObjectId (lowercase d). Import the wrong name and you get undefined back. Then new undefined() explodes.
// mongodb@4+ (current)
const { ObjectId } = require('mongodb');
// ESM
import { ObjectId } from 'mongodb';
// Mongoose
const { Types } = require('mongoose');
const id = new Types.ObjectId(rawId);
// Still on mongodb@3? Capital D:
const { ObjectID } = require('mongodb');
3. Fix the classic "wrong param name" case
Your route is /api/users/:userId but you're reading req.params.id. Result: undefined. This catches people constantly โ double-check that param names match end to end.
// Route: GET /api/users/:userId
router.get('/api/users/:userId', async (req, res) => {
const { userId } = req.params; // NOT req.params.id
if (!userId || !ObjectId.isValid(userId)) {
return res.status(400).json({ error: 'Invalid or missing user ID' });
}
const user = await db.collection('users').findOne({ _id: new ObjectId(userId) });
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
4. Wrap in try-catch for bulk operations
Processing 50 IDs in a batch? One bad entry shouldn't kill the whole request. A small helper filters the junk and keeps going:
function toObjectId(id) {
try {
return new ObjectId(id);
} catch {
return null;
}
}
const ids = rawIds.map(toObjectId).filter(Boolean);
const results = await db.collection('items').find({ _id: { $in: ids } }).toArray();
5. Watch out for isValid()'s quirk with 12-char strings
ObjectId.isValid() returns true for any 12-character string โ not just hex. Try it: ObjectId.isValid("hello world!") comes back true. For user-facing IDs where you need strict 24-hex validation, add a regex:
function isStrictObjectId(id) {
return typeof id === 'string' && /^[a-f\d]{24}$/i.test(id);
}
// Usage
if (!isStrictObjectId(req.params.id)) {
return res.status(400).json({ error: 'Invalid ID' });
}
Verify the Fix
Run this quick check โ each output should match the inline comment:
const { ObjectId } = require('mongodb');
// Valid โ should work
console.log(ObjectId.isValid('507f1f77bcf86cd799439011')); // true
console.log(new ObjectId('507f1f77bcf86cd799439011').toString()); // '507f1f77bcf86cd799439011'
// Invalid โ isValid() catches these before ObjectId() ever runs
console.log(ObjectId.isValid(undefined)); // false
console.log(ObjectId.isValid('')); // false
console.log(ObjectId.isValid('123')); // false
console.log(ObjectId.isValid('not-hex-data')); // false
All six outputs match? Your guard is solid.
Tips
- Centralize the check: Write one
isValidObjectId()helper in a shared utils file. One import everywhere beats copy-pasting the same three lines across a dozen route handlers. - Validate at the boundary: Check IDs in the route handler, not buried inside service functions. Catching it early means cleaner error messages and half the debugging time.
- Log the bad value: When the check fails, log exactly what came in โ
console.error('Invalid ObjectId:', rawId). You'll thank yourself the first time a client sends"undefined"as a literal string. - Mongoose CastError: Pass an invalid ID to
Model.findById(id)and Mongoose throws aCastError, not aBSONTypeError. Different error name, same root cause โ validate before querying either way. - Don't re-wrap existing ObjectIds: If
doc._idis already an ObjectId instance,new ObjectId(doc._id)technically works but adds pointless overhead. Use the instance directly in queries.

