The Pattern
``regex
^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$
`
Flags: gmi — multiline, so ^ and $ anchor to each line rather than the whole input.
What It Accepts and Rejects
| Input | Result |
|---|---|
SW1A 1AA | ✅ matches |
EC1A1BB | ✅ matches |
M1 1AE | ✅ matches |
12345 | ❌ rejected |
Outward and Inward
A UK postcode has two halves. The outward code is one or two letters, a digit, and an
optional further letter or digit; the inward code is always a digit followed by two letters.
The space between them is conventional and frequently missing in stored data, so the pattern
makes it optional with ? .
The i flag accepts lowercase input, which is how postcodes arrive from forms. Normalise
to uppercase with a single space before storing, so that sw1a1aa and SW1A 1AA compare
equal.
The Limits of This Pattern
This is a shape check, not a validity check. ZZ9 9ZZ matches the pattern and is not a
real postcode. The full specification also excludes certain letters in certain positions —
the inward code never uses C, I, K, M, O or V — and there are genuine special cases like
GIR 0AA for Girobank.
For anything that affects delivery or eligibility, check against Royal Mail's PAF or the free
Ordnance Survey open data rather than trusting the format.
Testing Before Shipping
A regex that has only been tried against inputs you expect to match is untested. Every
pattern needs three kinds of case:
1. Valid inputs that should match, including the awkward-but-legal ones.
2. Invalid inputs that should not, especially near-misses that differ by one character.
3. Adversarial inputs — very long strings, unusual Unicode, and nesting that could
trigger catastrophic backtracking.
Paste your own examples into the tester above and watch which lines highlight. A pattern
that matches everything you throw at it is usually too permissive rather than correct.
Catastrophic Backtracking
Nested quantifiers over overlapping character classes — (a+)+, (\w+\s?)* — can take
exponential time on a non-matching input. On a server that is a denial-of-service bug, not a
performance issue. If a pattern is applied to user input, bound the input length first and
prefer explicit alternation over nested repetition.
Anchors, Greediness and Backtracking
Three behaviours account for most regex surprises, and this pattern shows all three.
Anchors. ^ and $ pin the match to the start and end of the input. Without them,
\d{3} matches the 123 inside abc123def. With m in the flags — as here — they
pin to each *line* instead, which is what lets one pattern be tested against a list.
Greediness. .* takes as much as it can and gives back only when forced; .*? takes
as little as possible. On , the pattern <.*> matches the whole string and
<.*?> matches just .
Testing It Properly
`javascript
const pattern = /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/gmi;