GUID Generator JavaScript: crypto.randomUUID and Safe Fallbacks
JavaScript has had a built-in UUID generator since 2021, which makes most of the snippets still circulating on the web obsolete — and several of them were never safe to begin with. Here is the current answer, the fallback for the contexts that still need one, and the reasoning that tells you which to reach for.
Use crypto.randomUUID()
const id = crypto.randomUUID();
// '36b8f84d-df4e-4d49-b662-bcde71a8764f'
That is the whole implementation. It returns a standards-compliant v4 UUID as a 36-character lowercase string, using the platform's cryptographically secure random source. There is no dependency to install and no snippet to maintain.
The format is fixed by RFC 9562: eight hex digits, then three groups of four, then twelve, with the version nibble always 4 and the variant bits set. That gives 122 random bits — the other six carry version and variant.
GUID and UUID are the same thing. GUID is Microsoft's name for the format, UUID is the RFC's, and both describe the same 128-bit value. Anything that accepts one accepts the other, so a question about a GUID generator in JavaScript is answered by crypto.randomUUID().
Where it is available
Support is now effectively universal, but two environment details catch people out often enough to be worth stating precisely.
Chrome / Edge 92+ Firefox 95+ Safari 15.4+
Node 19+ globalThis.crypto.randomUUID()
Node 14.17 - 18 require('crypto').randomUUID()
Deno, Bun built in
In browsers crypto.randomUUID only exists in a secure context. That means HTTPS, or localhost. On http://192.168.1.10:3000 — a phone testing against your dev machine over the LAN — it is undefined, while the same code on http://localhost:3000 works. Nearly every report of crypto.randomUUID is not a function in development traces back to this.
// Node 14.17-18: crypto is not global
import { randomUUID } from 'node:crypto';
const id = randomUUID();
In Node before 19, crypto is not on the global object, so browser-shaped code fails on the server. If the same module runs in both places — SSR, an edge function, a shared utility — import from node:crypto or feature-detect rather than assuming a global.
Never generate UUIDs with Math.random
The widely copied snippet built on Math.random() produces UUIDs that look correct and are predictable.
// Do not use this
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
});
Math.random() is not cryptographically secure and was never intended to be. Engines implement it with a fast PRNG — xorshift128+ in V8 — seeded from a small amount of state. Given enough observed output, the internal state can be recovered and both future and prior values reconstructed. The V8 team has said explicitly that it must not be used for anything security-related.
That is harmless for a React list key and dangerous for a session token, a password-reset link, an invitation code, or any identifier an attacker benefits from guessing. The failure is silent: the output looks exactly like a real UUID, so nothing in testing reveals the problem.
Since crypto.randomUUID() is shorter to write and available everywhere the snippet runs, there is no remaining reason to keep it in a codebase.
A fallback that is actually secure
When you must support a context without randomUUID — an insecure origin, or an older runtime — build on crypto.getRandomValues, which has near-universal support and is a CSPRNG.
function uuidv4() {
if (globalThis.crypto?.randomUUID) return crypto.randomUUID();
const b = crypto.getRandomValues(new Uint8Array(16));
b[6] = (b[6] & 0x0f) | 0x40; // version 4
b[8] = (b[8] & 0x3f) | 0x80; // variant RFC 9562
const h = [...b].map((x) => x.toString(16).padStart(2, '0'));
return `${h.slice(0, 4).join('')}-${h.slice(4, 6).join('')}-` +
`${h.slice(6, 8).join('')}-${h.slice(8, 10).join('')}-` +
`${h.slice(10).join('')}`;
}
The two bit operations are not optional. They set the version nibble to 4 and the variant bits to the RFC value; skipping them produces a string that fails strict UUID validation in Postgres, in .NET and in most parsers, even though it looks fine.
padStart(2, '0') matters just as much. Without it, any byte below 0x10 renders as a single hex digit and the UUID comes out too short. With 14 random bytes per UUID, at least one falls below 0x10 about 59 percent of the time, so most of the generated values are malformed, yet each one still looks plausible at a glance until a strict parser rejects it.
Validating a UUID
Validation is where a permissive regex quietly lets malformed values through. This one checks version and variant rather than only shape.
const UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
UUID_RE.test('36b8f84d-df4e-4d49-b662-bcde71a8764f'); // true
UUID_RE.test('36b8f84d-df4e-0d49-b662-bcde71a8764f'); // false, version 0
The third group's first character is the version and the fourth group's first character encodes the variant, which is why they are constrained rather than left as [0-9a-f]. The nil UUID 00000000-0000-0000-0000-000000000000 is valid per the RFC but fails this pattern, so accept it explicitly if your data uses it as a sentinel.
When v4 is the wrong choice
A v4 UUID is random, and randomness is exactly what hurts when the value becomes a database primary key. Each insert lands at an arbitrary point in the index, which fragments B-tree pages and degrades write throughput as the table grows.
UUID v7 solves this by putting a millisecond timestamp in the high bits, so values generated later sort after earlier ones while staying globally unique. Inserts append to the end of the index the way an auto-increment key does, and you keep the ability to generate IDs client-side.
import { v7 as uuidv7 } from 'uuid';
const id = uuidv7();
// '018f4a2b-7c00-7000-8000-...' — time-ordered
There is no built-in v7 yet, so this is one of the cases where the package earns its place. The trade-off is that a v7 UUID leaks its creation time — fine for internal keys, worth thinking about for anything public.
On collisions: v4 has 122 random bits. Generating a billion per second for a whole year leaves roughly a 1-in-10,000 chance of a single duplicate, and it takes about 86 years at that rate to reach even odds. Practical collisions come from a weak random source, not from the format, which is the whole argument against the Math.random version.
When the uuid package is still worth it
The uuid npm package earns its place when you need versions the platform does not provide: v1, v5 name-based UUIDs derived deterministically from a namespace, v7 time-ordered identifiers, or parsing and stringifying between byte arrays and text.
For plain random identifiers in application code, the built-in is smaller, faster and one less dependency to audit. Dropping it usually removes the package entirely — worth checking, because it still appears in many bundles purely for a v4 call the platform now handles.
Frequently asked questions
Why is crypto.randomUUID undefined?
Almost always because the page is not in a secure context. Browsers expose it over HTTPS and on `localhost`, but not on a plain-HTTP LAN address such as `http://192.168.1.10:3000`. In Node before version 19, `crypto` is not global — import `randomUUID` from `node:crypto` instead.
Is a GUID the same as a UUID?
Yes. GUID is Microsoft's name and UUID is the RFC's name for the same 128-bit format. They are interchangeable, so `crypto.randomUUID()` is the answer to both.
Can I use Math.random to generate a UUID?
Not for anything that matters. `Math.random()` is a fast non-cryptographic PRNG whose internal state can be recovered from observed output, making the values predictable. It is acceptable for a throwaway DOM key and unsafe for tokens, reset links or invitation codes. Use `crypto.randomUUID()` or `crypto.getRandomValues()`.
Do I still need the uuid npm package?
Only for what the platform does not do: v1, v5 name-based UUIDs, v7 time-ordered IDs, or converting between byte arrays and strings. For plain v4 generation the built-in is faster and removes a dependency.
Should I use UUID v4 or v7 as a database primary key?
v7 for a primary key in a large table. Its leading timestamp makes values time-ordered, so inserts append to the index instead of scattering through it and fragmenting pages. Use v4 where unpredictability matters more than insert locality, and note that v7 reveals when the identifier was created.
How likely is a UUID collision?
Negligible for any real workload. Even a billion v4 UUIDs per second for a year leaves about a 1-in-10,000 chance of one duplicate. Real-world collisions come from a weak random source such as `Math.random`, not from the format itself.