Fix BSONTypeError: Argument passed in must be a string of 12 bytes or 24 hex characters when creating ObjectId

beginner๐Ÿƒ MongoDB2026-05-31| Node.js with mongodb npm package (v4+) or mongoose, BSON v4+, MongoDB 5.x / 6.x / 7.x

Error Message

BSONTypeError: Argument passed in must be a string of 12 bytes or a string of 24 hex characters or an integer
#mongodb#bson#objectid#nodejs

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 undefined because 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 you undefined
  • Passing a document's _id that's already an ObjectId instance back into new 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 a CastError, not a BSONTypeError. Different error name, same root cause โ€” validate before querying either way.
  • Don't re-wrap existing ObjectIds: If doc._id is already an ObjectId instance, new ObjectId(doc._id) technically works but adds pointless overhead. Use the instance directly in queries.

Related Error Notes