本页面有中文版。
Cette page est disponible en français.
Esta página está disponible en español.
Patterns1 live hunt inside

Password rules with regex lookaheads (and when not to)

How to check \"at least one uppercase letter, one digit, eight characters\" in a single regex with lookaheads, why the order-free version needs them, the traps in the common patterns, and why checking each rule separately is often the better design.

Regex Hunter logo

· 5 min read

/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/

match

  • Secret123
  • aB3defgh
  • Tr0ub4dor
  • 9Lives_cat

spare

  • secret123
  • SECRET123
  • Secretpass
  • Ab1

"At least 8 characters, with an uppercase letter, a lowercase letter and a digit." It's one of the most common things people ask a regex to do, and it's a good way to learn lookaheads, because the rules don't say where each character goes.

This post builds the pattern one rule at a time, covers the traps, and ends with a case for not using one big regex at all. Every example is live, so edit the patterns and watch the lines change.

Quick answerJavaScript
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/

At least 8 characters, with at least one lowercase letter, one uppercase letter and one digit, in any order.

Accepts

  • Secret123
  • 9Lives_cat
  • aB3defgh

Rejects

  • secret123
  • SECRET123
  • Ab1

01Why a plain pattern can't do it

A pattern reads left to right. To say "contains a digit" you'd write something like .*\d.*, and that's fine on its own. But "contains a digit and contains a capital" in any order turns into listing every order:

Pattern31 chars
/^(.*[A-Z].*\d.*|.*\d.*[A-Z].*)$/
Try it in the Playground
Two rules, two orders. Three rules would need six branches, four rules twenty-four.

That doesn't scale. You need a way to check a rule without moving through the text.

02Lookaheads check without moving

A lookahead (?=…) checks that something matches from the current position, then goes back to where it started. So several lookaheads at the start can each scan the whole password, one rule each.

One rule: "somewhere ahead there's a digit":

Matches3/3 as expected
/^(?=.*\d)/

Should match2/2

  • abc1
  • 1abc

Should not1/1

  • abcd
Try it in the Playground
(?=.*\d) looks ahead for "anything, then a digit". If it's there, the match succeeds at position 0.

The .* before \d is what makes "somewhere" work. Without it, (?=\d) would mean "the very next character is a digit".

03Stacking the rules

Each rule gets its own lookahead, all anchored at the start, then the length check consumes the text:

Matches7/7 as expected
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/

Should match3/3

  • Secret123
  • aB3defgh
  • 9Lives_cat

Should not4/4

  • secret123
  • SECRET123
  • Secretpass
  • Ab1
Try it in the Playground
Three lookaheads (a lowercase letter, a capital, a digit), then 8 or more characters of anything.

Read it as a list of conditions. Adding a rule is adding a lookahead, in any order:

Matches3/3 as expected
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/

Should match2/2

  • Secret123!
  • p@ssW0rd

Should not1/1

  • Secret123
Try it in the Playground
A fourth lookahead requires a symbol: any character that isn't a letter or digit.

A lookahead that forbids something is (?!…). "No spaces anywhere":

Matches2/2 as expected
/^(?!.*\s)(?=.*\d).{8,}$/

Should match1/1

  • correct1horse

Should not1/1

  • correct 1 horse
Try it in the Playground
(?!.*\s) fails if a whitespace character appears anywhere ahead.

04Traps in the common patterns

Forgetting the anchors. Without ^ and $, .{8,} only needs eight characters somewhere, and every lookahead runs from wherever the match happens to start. Keep ^ at the front so all the lookaheads start at position 0, and $ at the end so the length check covers the whole string.

A maximum length that doesn't work. .{8,64} without $ happily matches the first 64 characters of a 500-character password. With $ it rejects it, which is what you meant.

Too narrow a "symbol". A class like [!@#$%^&*] rejects ?, ~, -, spaces and every non-English character. [^A-Za-z0-9] ("not a letter or digit") is what people usually mean.

Letters outside A to Z. [A-Z] doesn't count É as a capital. With the u flag, \p{Lu} and \p{Ll} are any uppercase and lowercase letter.

05When not to

One regex gives one answer: pass or fail. Your sign-up form needs to tell the person which rule they missed:

const rules = [
  { test: (p) => p.length >= 12, message: "Use at least 12 characters" },
  { test: (p) => /\d/.test(p),   message: "Add a number" },
  { test: (p) => /[A-Z]/.test(p), message: "Add a capital letter" },
];

const problems = rules.filter((r) => !r.test(password)).map((r) => r.message);

Each check is tiny, readable and easy to change, and the error messages come for free.

It's also worth asking whether you want composition rules at all. NIST's guidelines for passwords (SP 800-63B) advise against rules like "one capital, one digit, one symbol". They push people towards Password1!. The guidance favours length, allowing long passphrases (at least 64 characters, spaces included), and checking new passwords against lists of known breached ones.

So the lookahead pattern is worth knowing, both for passwords and for any "must contain all of these, in any order" check. For a real sign-up form, a minimum length plus a breached-password check usually beats it.

06Try one

Catch the strings that contain both a digit and a letter, in any order:

Live huntpar 20

Catch strings with at least one letter and at least one digit, in any order.

Targets · 0/6 caught

  • a1target
  • 1atarget
  • abc123target
  • 123abctarget
  • x9ytarget
  • R2D2target

Innocents · 0 hit

  • abcinnocent
  • 123innocent
  • ---innocent
  • a-binnocent
  • 4-5innocent
  • !!innocent

Type a regex — the board updates as you go.

Try it in the Playground
Like this? A new one drops every day.Play today’s hunt

07Checklist

  • (?=.*X) means "X appears somewhere ahead". Stack one per rule.
  • (?!.*X) means "X appears nowhere ahead".
  • Anchor with ^ so every lookahead starts at position 0, and with $ so a length limit covers everything.
  • [^A-Za-z0-9] is a better "symbol" than a hand-picked list.
  • For forms, separate checks give separate error messages.
  • Length and a breached-password check beat composition rules.

08Keep practising