Skip to content

Error Handling

validatePassword doesn't throw — it returns a structured result with a boolean verdict, a strength score, and human-readable feedback. This guide documents the actual shape of that result and the patterns for handling it in practice. Per-message severity is still on the roadmap; stable error codes and custom message overrides shipped in v1.2.0 — see the i18n guide and Core API.

What you get back

typescript
interface ValidationResult {
  valid: boolean
  score: StrengthScore // 0 | 1 | 2 | 3 | 4
  strength: StrengthLabel // 'very-weak' | 'weak' | 'medium' | 'strong' | 'very-strong'
  feedback: {
    warning?: string
    suggestions: readonly string[]
  }
  checks: Record<CheckId, boolean>
}
FieldUse it for
validThe single source of truth for "is this password acceptable?" Use it to gate submit.
score / strengthUX cues — strength meter, color coding. Not acceptance decisions: strength can be 'very-strong' while valid is false. See Scoring caveat below.
feedback.warningThe first suggestion in aggregator order, surfaced for prominent display.
feedback.suggestionsAll failure messages, in aggregator order. Each entry is a rendered string (default English unless you supplied messages / formatMessage). validatePassword does not expose per-failure MessageCodes in its return shape — use result.checks for coarse per-validator pass/fail booleans, or call the individual validators yourself to read ValidatorCheck.code and ValidatorCheck.params. Per-message severity is not yet exposed.
checksRecord<CheckId, boolean> — keyed by CheckId ('length', 'characterTypes', etc.), not by MessageCode. Inspect this when you want to know which validator rejected the input. length: false tells you the length check failed; it does not tell you whether it was length.tooShort or length.tooLong. Read ValidatorCheck.code from the individual validator if you need that distinction.

Acceptance gating

Always gate submit on valid, never on strength:

tsx
const result = validatePassword(password, options)

// ✅ Correct: valid is true only if every check passed.
if (!result.valid) return

// ❌ Wrong: 'strong' or 'very-strong' can both still have valid=false.
if (result.strength === 'strong' || result.strength === 'very-strong') {
  /* … */
}

Surfacing failures to the user

feedback.suggestions is the list to render. Each entry is a plain English string suitable for display. The feedback.warning field is just suggestions[0] — surface it prominently if you want a single "headline" message:

tsx
{result && !result.valid && (
  <div role="alert" aria-live="polite">
    {result.feedback.warning && <p className="error-headline">{result.feedback.warning}</p>}
    <ul>
      {result.feedback.suggestions.map((msg, i) => (
        <li key={i}>{msg}</li>
      ))}
    </ul>
  </div>
)}

For accessibility, the wrapping element should be a live region (role="alert" aria-live="polite") so screen readers announce updates as the user types.

Aggregator ordering

validatePassword runs all seven validators on every call, in this fixed order, and collects failure messages into suggestions:

  1. length
  2. characterTypes
  3. repetition
  4. sequential
  5. commonPassword
  6. personalInfo
  7. keyboardPattern

So feedback.warning (== suggestions[0]) is always the first failure in this order, not the most important one. For an input like 'john1234!' with personalInfo: ['john@example.com'], three checks fail (sequential, personalInfo, keyboardPattern) — warning is the sequential message because sequential runs before personalInfo in the order, not because sequential is "more critical."

If you need to react to a specific failure, inspect result.checks:

tsx
if (!result.checks.commonPassword) {
  // Flag rejected by the common-password validator specifically.
}

Scoring caveat

score is purely a passed-check ratio: Math.min(4, Math.floor((passedChecks / 7) * 5)). So a password that fails only one check (e.g., is a known common password but otherwise excellent) still scores 4 / 'very-strong' while valid is false. The strength label communicates "how much of the policy this passes," not "is this acceptable." Treat them as orthogonal signals.

Server-side handling

For backend code, validatePassword returns the same shape — render feedback.warning / feedback.suggestions as the response body and let the client display them. See the Server-Side Usage guide for the full Express / Fastify / NestJS patterns.

Critical: never log the password or the full result in production. The result includes password-derived inferences (checks, suggestions) — leaking the failure shape from logs can help an attacker who later obtains the logs. Store only the bit you act on (valid):

typescript
const result = validatePassword(req.body.password, options)
if (!result.valid) {
  return res.status(400).json({
    suggestions: result.feedback.suggestions, // OK to send to the requester
  })
}
// Do NOT: logger.info({ result }) — captures the failure shape in logs.

What's shipped, what's not

Shipped in v1.2.0:

  • Stable error codes on each ValidatorCheck.code ('length.tooShort', 'characterTypes.missing', etc.) plus params for interpolation values. See MessageCode.
  • Custom message overrides via ValidatorOptions.messages (template map) and ValidatorOptions.formatMessage (callback for react-intl / i18next / ICU). See the i18n guide.

Still on the roadmap (does not exist yet):

  • Per-message severity. All suggestions are at the same level. The ValidationMessageSeverity type in @sentinel-password/react-components includes 'warning' | 'error' | 'success', but the PasswordInput component only ever emits 'warning' and 'error' today (see react-components API: Validation Messages).

See Also

Released under the MIT License.