15 regex interview questions, with worked solutions
Fifteen regex exercises of the kind interviewers ask, from validating a username to explaining why a pattern freezes a server, each with a solution, the edge cases you'll be pushed on and notes on performance.

· 14 min read
/^(?!.*__)[a-z][a-z0-9_]{1,14}[a-z0-9]$/
match
- night_owl
- dev42
- x_ray
spare
- _admin
- 9lives
- owl_
Good regex interview questions check whether you can write a pattern that is correct, readable and safe on bad input: validating a username, reformatting dates, pulling fields out of a log line, masking a card number, and explaining why a pattern freezes a server. Below are 15 such exercises with worked solutions, the edge cases an interviewer will push on, and notes on performance where it matters.
Every pattern here is JavaScript, and every example is live: matches are highlighted as you read. Where Python or Java behave differently, the solution says so.
01Validate a username
The task: 3 to 16 characters; lowercase letters, digits and underscores; it starts with a letter, doesn't end with an underscore, and never has two underscores in a row.
/^(?!.*__)[a-z][a-z0-9_]{1,14}[a-z0-9]$/Should match3/3
- night_owl
- dev42
- x_ray
Should not6/6
- _admin
- 9lives
- night__owl
- owl_
- ab
- Night_owl
Why it works: the first and last characters have their own classes, and the middle takes 1 to 14, so the total is 3 to 16. The lookahead (?!.*__) checks the whole string once for a double underscore before anything is consumed.
Edge cases: without ^ and $, !!night_owl!! passes. Avoid \w here: it allows capitals, and in Python 3 it matches letters from any script.
02Reformat dates from DD/MM/YYYY to YYYY-MM-DD
The task: rewrite every date in a block of text, leaving everything else alone.
/\b(\d{2})\/(\d{2})\/(\d{4})\b/Should match2/2
- Due 04/11/2026
- From 05/01/2027 to 12/01/2027
Should not3/3
- Already ISO: 2026-11-04
- 4/11/2026
- Order 104/11/20261
The replacement puts the groups back in a new order:
text.replace(/\b(\d{2})\/(\d{2})\/(\d{4})\b/g, '$3-$2-$1')
// "Due 04/11/2026" → "Due 2026-11-04"
With named groups the replacement reads itself: (?<d>\d{2})\/(?<m>\d{2})\/(?<y>\d{4}) and '$<y>-$<m>-$<d>'. In Python the same is \g<y>-\g<m>-\g<d> with (?P<y>…) groups.
Edge cases: the \b on both sides stops a match inside a longer number. A regex checks the shape, not the calendar: 31/02/2026 passes, so check real dates with a date library afterwards. More on that in Regex for dates.
03Pull fields out of an access log line
The task: from a web server's log line, get the IP, time, method, path, status and size.
/^(?<ip>\S+) \S+ \S+ \[(?<time>[^\]]+)\] "(?<method>[A-Z]+) (?<path>\S+) [^"]*" (?<status>\d{3}) (?<bytes>\d+|-)$/Should match2/2
- 203.0.113.9 - - [04/Nov/2026:10:15:32 +0000] "GET /blog/regex-games HTTP/1.1" 200 5123
- 198.51.100.4 - - [04/Nov/2026:10:16:01 +0000] "POST /api/login HTTP/2.0" 401 -
Should not1/1
- 203.0.113.9 - - [04/Nov/2026:10:15:32 +0000] "GET / HTTP/1.1"
Why it works: each field is matched by what it can't contain: \S+ for a field without spaces, [^\]]+ up to the closing bracket, [^"]* up to the closing quote. Named groups make the result self-describing: line.match(re).groups.status is "200".
Performance: negated classes never overshoot, so a bad line fails quickly. The tempting version with .* between fields can try many ways to split a long line before it gives up.
Edge cases: a malformed request is logged as "-", which this pattern rejects; decide whether to skip those lines or add |- for them.
04Keep only the lines that don't mention DEBUG
The task: filter a log down to lines without the word DEBUG.
/^(?!.*\bDEBUG\b).*$/mShould match2/2
- INFO server started
- DEBUGGER attached
Should not2/2
- DEBUG cache miss
- warn: DEBUG off
Why it works: the negative lookahead scans the line for the word first; only if it isn't there does .* take the line. The \b on both sides keeps DEBUGGER in.
What to say next: in real code, lines.filter(l => !/\bDEBUG\b/.test(l)) or grep -v -w DEBUG is clearer. Knowing both is the point of the question.
05Get the text inside square brackets
The task: from [error] disk full [code 28], get error and code 28.
/\[[^\]]*\]/Should match2/2
- [error] disk full [code 28]
- []
Should not2/2
- no brackets here
- [unclosed
The trap: \[(.*)\] is greedy. It runs to the last ] and returns error] disk full [code 28. The lazy \[(.*?)\] works, but [^\]]* says exactly what you mean and never backtracks. Greedy vs lazy quantifiers has more.
06Find TODO comments that name an owner
The task: list // TODO(name): note comments, with the owner and the note.
///\s*TODO\((\w+)\):\s*(.+)/Should match2/2
- // TODO(ola): retry on timeout
- x = 1 //TODO(dev_ops): remove
Should not3/3
- // TODO: no owner
- // todo(ola): lowercase
- # TODO(ola): python comment
Edge cases: add the i flag if your team writes todo. To cover # comments too, start with (?://|#). And say it out loud: a regex can't tell a comment from a string that contains //, so a linter rule is the robust answer.
07Mask a card number except the last four digits
The task: turn 4111111111111111 into ************1111.
/\d(?=\d{4})/Should match2/2
- 4111111111111111
- card 5500000000000004
Should not2/2
- 1234
- no digits
'4111111111111111'.replace(/\d(?=\d{4})/g, '*') // "************1111"
Why it works: the lookahead (?=\d{4}) matches a digit only when four more digits follow, without consuming them, so the last four are never replaced.
Edge cases: with spaces, 4111 1111 1111 1111, nothing is masked, because no digit is followed directly by four digits. \d(?=(?:\D*\d){4}) lets separators sit in between and gives **** **** **** 1111.
08Convert camelCase to snake_case
The task: getUserId → get_user_id, and parseHTTPResponse → parse_http_response.
/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/s.replace(/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/g, '_').toLowerCase()
Why it works: the pattern matches no characters, only positions: after a lowercase letter or digit and before a capital, or inside a run of capitals just before the last one (the P in HTTPResponse). Only the first half gives parse_httpresponse, a common wrong answer.
Edge cases: lookbehind is in every current browser, Node and Python (Python's re needs it fixed-width, as here). Without lookbehind, capture the letter instead: ([a-z0-9])([A-Z]) → '$1_$2'.
09Validate a duration like 1h30m
The task: accept 1h30m, 45m and 2h, but not an empty string.
/^(?=\d)(?:(\d+)h)?(?:(\d+)m)?$/Should match3/3
- 1h30m
- 45m
- 2h
Should not4/4
- h
- 30m1h
- 1h 30m
- 90
Why it works: both parts are optional, so without the lookahead the pattern also accepts the empty string. (?=\d) says "something must be here, and it starts with a digit". It's a common trick whenever every part of a pattern is optional.
Edge cases: 1h90m passes. If minutes must be under 60, use ([0-5]?\d)m, or check the number in code, which reads better.
10Find Markdown headings, levels 1 to 3
The task: in a Markdown file, find #, ## and ### headings and capture their text.
/^(#{1,3})\s+(\S.*)$/mShould match2/2
- # Title
- ### Install
Should not3/3
- #### Too deep
- #hashtag
- Not a # heading
Why it works: #{1,3} can't stretch to four, because \s+ must come right after. The m flag makes ^ and $ work per line in a whole document (see Anchors and the m flag).
Edge cases: a # line inside a code block is not a heading, and a regex can't see code blocks. That's the moment to mention a Markdown parser.
11Split a CSV line that has quoted fields
The task: split widget,"Sizes 2, 4 and 6",9.99 into three fields, keeping the comma inside the quotes.
/(?:^|,)("(?:[^"]|"")*"|[^,]*)/With the g flag, group 1 of each match is a field: widget, "Sizes 2, 4 and 6" and 9.99. Strip the outer quotes and turn "" into " afterwards.
What to say next: quoted fields can contain line breaks, which a line-by-line regex can't handle. For real CSV, use a parser (Python's csv module, Papa Parse in JavaScript). A good interview answer gives the regex and says that.
12Find characters outside ASCII
The task: flag text that contains accented letters, symbols like €, or emoji, for example before writing to a system that only takes ASCII.
/[^\x00-\x7F]/Should match3/3
- café
- naïve résumé
- price: 5 €
Should not2/2
- plain ASCII text
- 100% ok!
Edge cases: without the u flag, an emoji is two UTF-16 units and both match; with u it's one character. To allow only printable ASCII, flip it: ^[\x20-\x7E]*$.
13Validate one label of a hostname
The task: a label (the part between dots) is 1 to 63 letters, digits or hyphens, and can't start or end with a hyphen.
/^(?!-)[a-z0-9-]{1,63}(?<!-)$/iShould match3/3
- regexhunter
- my-site-2
- a
Should not4/4
- -start
- end-
- under_score
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Why it works: the lookahead and lookbehind guard the two ends, so the length limit stays in one place, {1,63}. The last line is 64 characters, one too many.
Edge cases: without lookarounds it's ^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$, which is correct but harder to check. A full hostname is labels joined by dots, at most 253 characters, and that limit is simpler to check in code.
14Explain why a pattern freezes the server
The task: this check for "words separated by single spaces" works in tests, but some inputs freeze the server. Why, and how do you fix it?
^(\w+\s?)+$
The answer: the space is optional, so a run of letters like aaaaaaaa can be split between repetitions of the group in a huge number of ways (a|aaaaaaa, aa|aaaaaa, a|a|aaaaaa…). When the string ends in something that can't match, like !, the engine tries every split before it gives up. The work doubles with each extra letter: in Node, 24 letters and a ! took 8 seconds. This is catastrophic backtracking.
The fix: make sure each character can be matched in only one way. Make the separator required, and allow one optional space at the end:
/^\w+(?:\s\w+)*\s?$/Should match2/2
- hello world
- one two three
Should not2/2
- hello world
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!
The fixed pattern rejects 30,000 letters and a ! in about a millisecond. Other good answers: an atomic group or possessive quantifier where the engine has one (Java, PCRE, Python 3.11+), or a linear-time engine like RE2 for untrusted input. The full story is in Catastrophic backtracking.
15Strip HTML tags, and know when to stop
The task: turn <p>Hello <b>world</b></p> into Hello world.
text.replace(/<[^>]+>/g, '') works on simple markup. The third line shows where it breaks: plain text with < and > loses < 5 and 7 >. An attribute like title="x > y" breaks it too.
The answer interviewers want: HTML nests, and nesting is exactly what regular expressions can't count. To get text out of HTML, use the browser's own parser (new DOMParser() and textContent), and to make HTML safe, use a sanitizer library. A regex is fine for a quick look at trusted markup, never for security.
16Practice for the interview
Reading solutions helps; writing patterns under a little pressure helps more.
- The practice packs drill one technique per pack, and most levels are patterns you write at work: formats, log lines and identifiers in code.
- The daily hunt takes five minutes and keeps your regex warm between interviews.
- If the interview is in Python, read JavaScript vs Python regex for the differences that trip people up.