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
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>
}| Field | Use it for |
|---|---|
valid | The single source of truth for "is this password acceptable?" Use it to gate submit. |
score / strength | UX cues — strength meter, color coding. Not acceptance decisions: strength can be 'very-strong' while valid is false. See Scoring caveat below. |
feedback.warning | The first suggestion in aggregator order, surfaced for prominent display. |
feedback.suggestions | All 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. |
checks | Record<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:
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:
{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:
lengthcharacterTypesrepetitionsequentialcommonPasswordpersonalInfokeyboardPattern
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:
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):
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.) plusparamsfor interpolation values. SeeMessageCode. - Custom message overrides via
ValidatorOptions.messages(template map) andValidatorOptions.formatMessage(callback for react-intl / i18next / ICU). See the i18n guide.
Still on the roadmap (does not exist yet):
- Per-message severity. All
suggestionsare at the same level. TheValidationMessageSeveritytype in@sentinel-password/react-componentsincludes'warning' | 'error' | 'success', but thePasswordInputcomponent only ever emits'warning'and'error'today (see react-components API: Validation Messages).
See Also
- Validators — what each check rejects and how to relax it
- Core API — the full
ValidationResultand option types - Internationalization — translating the English messages
- Server-Side Usage — handling results outside the browser