A JavaScript string's length is not its UTF-8 payload size. That difference matters when a queue, file format or service imposes a byte limit. Serialize the value once, encode the result and check the buffer that you will actually send.

Prerequisites are a supported Node.js release with ES modules and the built-in assertion module. The APIs used here are documented for Node.js 24; the example was executed on the installed Node.js 26 runtime. No packages, HTTP server or external service are needed.

Complete example

Save as bytes.mjs:

import assert from 'node:assert/strict';
import { Buffer } from 'node:buffer';
function encodeWithin(value, limit) {
  if (!Number.isSafeInteger(limit) || limit < 0) throw new RangeError('limit');
  const json = JSON.stringify(value);
  if (json === undefined) throw new TypeError('not a JSON value');
  const bytes = Buffer.from(json, 'utf8');
  if (bytes.length > limit) throw new RangeError('payload too large');
  return bytes;
}
const value = {text: 'πŸ˜€'};
const bytes = encodeWithin(value, 15);
assert.equal(bytes.length, 15);
assert.equal(JSON.stringify(value).length, 13);
assert.deepEqual(JSON.parse(bytes.toString('utf8')), value);
assert.throws(() => encodeWithin(value, 14), RangeError);
assert.throws(() => encodeWithin(undefined, 100), TypeError);
assert.throws(() => encodeWithin({n: 1n}, 100), TypeError);
assert.throws(() => encodeWithin({}, -1), RangeError);
console.log('UTF-8 bytes=15; exact limit accepted; invalid cases rejected');

Run and expected output

node bytes.mjs
UTF-8 bytes=15; exact limit accepted; invalid cases rejected

The fixture's JSON has 13 JavaScript string units but occupies 15 UTF-8 bytes. A 15-byte limit accepts it; a 14-byte limit rejects it. Returning the buffer lets the caller pass the checked representation to a file or transport without a second serialization.

That single serialization is particularly useful when objects have getters or custom toJSON behavior. Counting one representation and later creating another can lead to inconsistent results. Keep the returned buffer private to the send operation, since buffers themselves remain mutable.

Edge cases and troubleshooting

Top-level undefined does not produce a JSON string, so it is rejected explicitly. BigInt values and circular references can cause serialization to throw; this function allows those errors to reach the caller. It is a serialization-and-size boundary, not a validator that guarantees every original property survives JSON conversion.

The check happens after serialization and encoding. It therefore bounds accepted output size, but does not prevent allocation of an oversized intermediate string or buffer. For untrusted, potentially huge inputs, impose limits earlier or use an appropriately bounded streaming design.

Confirm which bytes an external limit counts. Headers, compression, multipart envelopes and base64 wrapping can change the final wire size. The local test measures the plain JSON body only. If a service rejects a payload that passed this function, inspect the surrounding envelope before increasing the limit.

Primary reference

See Node.js documentation for Buffer encoding and byte lengths. Continue with node.js tutorials.