NewSortable IDs are here
prefID
Menu

ensureUnique()

Retry generation against your own store until a free ID is found — a hard uniqueness guarantee, without state in the library.

Signature

ts
function ensureUnique<T extends string>(
  generate: () => T,
  exists: (candidate: T) => boolean | Promise<boolean>,
  options?: { maxAttempts?: number } // default: 5
): Promise<T>

Why

Random IDs are collision-resistant by probability (see Uniqueness & Collisions). When you want a guarantee, check each candidate against your source of truth and regenerate on the astronomically rare clash. ensureUnique runs that loop for you — and preserves the generator's typed return value.

Usage

import { ensureUnique, id } from "prefid";

const userId = await ensureUnique(
  () => id("user"),
  (candidate) => db.users.exists(candidate),
);
// userId is typed `user_${string}` and confirmed free in your store
const { ensureUnique, id } = require("prefid");

const userId = await ensureUnique(
  () => id("user"),
  (candidate) => db.users.exists(candidate),
);
// userId is a unique user_… id, confirmed free in your store

Works with any store

You supply the exists check, so ensureUnique stays dependency-free and works with any database, cache, or in-memory structure. The check may be synchronous or return a promise.

ts
// Prisma
ensureUnique(() => id("user"), (c) =>
  prisma.user.count({ where: { id: c } }).then((n) => n > 0),
);

// Redis
ensureUnique(() => id("user"), (c) => redis.exists(c).then(Boolean));

// A plain Set (tests / demos)
ensureUnique(() => id("user"), (c) => seen.has(c));

Parameters

  • generate — produces a candidate ID, e.g. () => id("user").
  • exists — returns true if the candidate is already taken. Sync or async.
  • options.maxAttempts — how many candidates to try before giving up. Defaults to 5.

The loop guard

If exists keeps reporting collisions — usually a bug that always returns true — the function throws after maxAttempts instead of looping forever. That turns a hung server into a clear, catchable error.

Returns & errors

  • Resolves to a free ID (typed the same as generate).
  • Rejects with a RangeError if maxAttempts is not a positive integer.
  • Rejects with an Error if no free ID is found within maxAttempts.