payloadcms/payload · error · ValidationError

Value must be unique

Error message

Value must be unique

What it means

Thrown by handleUpsertError after a DB upsert fails with a UNIQUE constraint violation. The adapter parses the constraint-failure message (SQLite 'UNIQUE constraint failed: table.column' or Postgres unique-violation) to recover the offending field name via adapter.fieldConstraints, then re-throws as a Payload ValidationError with field-level details so the API returns a structured validation error instead of a raw DB error.

Source

Thrown at packages/drizzle/src/upsertRow/handleUpsertError.ts:76

        if (match && match[1]) {
          fieldName = match[1]
        }
      }
    } else if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
      // SQLite - extract from message: "UNIQUE constraint failed: table.field[, table.field2, ...]"
      const regex = /UNIQUE constraint failed: ([^.]+)\.([^.,]+)/
      const match: string[] = error.message?.match(regex)
      if (match && match[2]) {
        if (adapter.fieldConstraints[tableName]) {
          fieldName = adapter.fieldConstraints[tableName][`${match[2]}_idx`]
        }
        if (!fieldName) {
          fieldName = match[2]
        }
      }
    }

    throw new ValidationError(
      {
        id,
        collection: collectionSlug,
        errors: [
          {
            message: req?.t ? req.t('error:valueMustBeUnique') : 'Value must be unique',
            path: fieldName,
            tableName,
          },
        ],
        global: globalSlug,
        req,
      },
      req?.t,
    )
  }

  // Re-throw non-constraint errors

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Validate uniqueness in your application/validation hook before attempting the upsert (with a tolerance for races).
  2. Prepend a select to check existence and branch to update instead of relying on upsert to detect duplicates.
  3. Catch the ValidationError at the API boundary and return a 422 with the offending field path to the client.

Example fix

// before
await payload.create({ collection: 'users', data: { email } })
// after
try {
  await payload.create({ collection: 'users', data: { email } })
} catch (err) {
  if (err.data?.errors?.some(e => e.message === 'Value must be unique')) {
    return res.status(409).json({ error: 'email already in use' })
  }
  throw err
}
Defensive patterns

Strategy: try-catch

Validate before calling

async function isUnique(collection, field, value, payload) {
  const { docs } = await payload.find({ collection, where: { [field]: { equals: value } }, limit: 1 })
  return docs.length === 0
}

Try / catch

try {
  await payload.create({ collection, data })
} catch (err) {
  const isUniqueErr = err?.data?.errors?.some(e => /unique/i.test(e.message))
  if (isUniqueErr) return res.status(409).json({ error: 'duplicate value' })
  throw err
}

Prevention

When it happens

Trigger: An upsert (createOrUpdate) where the incoming row collides with an existing row on a unique-indexed column, e.g. duplicate email, slug, or username.

Common situations: Two concurrent requests creating the same unique value; seeding/importing data with duplicate keys; a unique field that the client did not validate before submit.

Related errors


AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12). Data as JSON: /api/errors/fccf64a943b3a0cd. Report an issue: GitHub.