Regex Newline Behavior in JavaScript
Handling newlines in regular expressions is one of the most confusing aspects of pattern matching. The behavior of metacharacters like ., ^, and $ changes dramatically based on regex flags, and different programming languages implement newline handling differently.
This guide focuses on JavaScript's regex newline behavior, covering the DotAll (s) and Multiline (m) flags, common gotchas, and practical patterns for matching text that spans multiple lines.
The Dot (.) Metacharacter
By default, the dot (.) matches any character except newline characters (\n, \r).
Default Behavior
const text = "Hello\nWorld";
const regex = /Hello.World/;
console.log(regex.test(text)); // false
// The dot doesn't match the newline characterThis default behavior is intentional. In most text processing scenarios, you want to match patterns within a single line, not across line boundaries.
Using the DotAll Flag (s)
The s flag (also called "dotAll" or "single-line") makes the dot match newline characters:
const text = "Hello\nWorld";
const regex = /Hello.World/s; // Note the 's' flag
console.log(regex.test(text)); // true
// Now the dot matches the newlineImportant: The s flag was introduced in ES2018. For older browsers, use [\s\S] or [^] instead of ..
Anchors: ^ and $
The anchors ^ (start) and $ (end) also behave differently with newlines.
Default Behavior
Without the multiline flag, ^ matches the start of the entire string, and $ matches the end:
const text = "Line 1\nLine 2\nLine 3";
const regex = /^Line 2/;
console.log(regex.test(text)); // false
// ^Line 2 looks for "Line 2" at the start of the string
// but the string starts with "Line 1"Using the Multiline Flag (m)
The m flag changes ^ and $ to match the start and end of each line:
const text = "Line 1\nLine 2\nLine 3";
const regex = /^Line 2/m; // Note the 'm' flag
console.log(regex.test(text)); // true
// Now ^Line 2 matches "Line 2" at the start of the second linePractical Example: Extracting Lines
const log = `
[INFO] Application started
[ERROR] Database connection failed
[INFO] Retrying...
[ERROR] Timeout after 30s
`;
// Extract all ERROR lines
const errorRegex = /^\[ERROR\].*$/gm;
const errors = log.match(errorRegex);
console.log(errors);
// ["[ERROR] Database connection failed", "[ERROR] Timeout after 30s"]Common Gotchas
Gotcha 1: Confusing s and m Flags
The naming is counterintuitive:
sflag = "single-line mode" but makes.match newlines (useful for multi-line matching)mflag = "multi-line mode" and affects^and$anchors
These flags are independent and can be combined:
const regex = /^.*$/sm; // Both flags together
// s: dot matches newlines
// m: ^ and $ match line boundariesGotcha 2: Different Newline Characters
Different operating systems use different newline sequences:
- Unix/Linux:
\n(LF) - Windows:
\r\n(CRLF) - Old Mac:
\r(CR)
To match any newline variation:
// Match any newline sequence
const anyNewline = /\r?\n|\r/g;
// Split by any newline
const lines = text.split(/\r?\n|\r/);
// Alternative: use a more comprehensive pattern
const lines2 = text.split(/\r\n|\n|\r/);Gotcha 3: Greedy Matching Across Lines
When using the s flag, greedy quantifiers can match too much:
const html = `
<div>First</div>
<div>Second</div>
<div>Third</div>
`;
// Trying to match individual <div> blocks
const greedy = /<div>.*<\/div>/s;
console.log(html.match(greedy)[0]);
// Matches from first <div> to LAST </div> - the entire string!
// Use non-greedy matching instead
const nonGreedy = /<div>.*?<\/div>/gs;
console.log(html.match(nonGreedy));
// ["<div>First</div>", "<div>Second</div>", "<div>Third</div>"]Gotcha 4: Unicode Line Terminators
JavaScript's ^ and $ (with m flag) recognize several Unicode line terminators:
\n(U+000A) - Line Feed\r(U+000D) - Carriage Return\u2028- Line Separator\u2029- Paragraph Separator
const text = "Line 1\u2028Line 2"; // Unicode line separator
const regex = /^Line 2/m;
console.log(regex.test(text)); // truePractical Patterns
Pattern 1: Match Everything Between Markers
// Extract content between START and END markers (including newlines)
const text = `
Some text
START
Important content
spanning multiple
lines
END
More text
`;
const pattern = /START([\s\S]*?)END/;
const match = text.match(pattern);
console.log(match[1].trim());
// "Important content\nspanning multiple\nlines"
// Modern alternative with s flag
const pattern2 = /START(.*?)END/s;Pattern 2: Match Lines Starting With Specific Text
const log = `
2026-01-11 10:00:00 INFO Server started
2026-01-11 10:00:15 ERROR Connection failed
2026-01-11 10:00:20 INFO Retrying
2026-01-11 10:00:25 ERROR Timeout
`;
// Get all ERROR lines
const errors = log.match(/^.*ERROR.*$/gm);
console.log(errors);Pattern 3: Match Multi-line Code Blocks
const markdown = `
Some text
```javascript
function hello() {
console.log("Hello");
}
```
More text
`;
// Extract code blocks
const codeBlocks = /```\w*\n([\s\S]*?)\n```/g;
const matches = [...markdown.matchAll(codeBlocks)];
matches.forEach(match => {
console.log("Code block:", match[1]);
});Pattern 4: Split Text by Blank Lines
const text = `
Paragraph 1
with multiple lines
Paragraph 2
also with
multiple lines
Paragraph 3
`;
// Split by one or more blank lines
const paragraphs = text.split(/\n\s*\n/);
console.log(paragraphs.map(p => p.trim()));Cross-Language Differences
Be aware that newline handling differs across languages:
Python
import re
# Python's dot doesn't match newlines by default
pattern = re.compile(r'Hello.World')
# Use re.DOTALL flag (or re.S)
pattern_dotall = re.compile(r'Hello.World', re.DOTALL)
# Python's ^ and $ match line boundaries by default with re.MULTILINE
pattern_multiline = re.compile(r'^Line 2', re.MULTILINE)Java
// Java dot doesn't match newlines by default
Pattern pattern = Pattern.compile("Hello.World");
// Use DOTALL flag
Pattern dotallPattern = Pattern.compile("Hello.World", Pattern.DOTALL);
// Use MULTILINE for ^ and $ to match line boundaries
Pattern multilinePattern = Pattern.compile("^Line 2", Pattern.MULTILINE);Go
// Go's dot doesn't match newlines by default
pattern := regexp.MustCompile("Hello.World")
// Use (?s) inline flag for dotall
dotallPattern := regexp.MustCompile("(?s)Hello.World")
// Use (?m) for multiline
multilinePattern := regexp.MustCompile("(?m)^Line 2")Testing Your Patterns
When working with complex newline patterns, use the DevToys Pro RegEx Tester to:
- Test patterns with different flag combinations
- Visualize matches across multiple lines
- Debug greedy vs. non-greedy matching behavior
- Experiment with different newline sequences
Best Practices
1. Be Explicit About Flags
// Bad: unclear what behavior you want
const pattern = /start.*end/;
// Good: explicit about newline handling
const pattern = /start.*?end/s; // s flag: dot matches newlines
const pattern2 = /^start.*end$/m; // m flag: ^ and $ per line2. Use [\s\S] for Broad Compatibility
// If s flag support is uncertain, use [\s\S]
const pattern = /start([\s\S]*?)end/;
// [\s\S] means "any whitespace or non-whitespace" = any character3. Normalize Line Endings When Possible
// Normalize to \n before processing
const normalized = text.replace(/\r\n|\r/g, '\n');
// Now you can use simpler patterns
const lines = normalized.split('\n');4. Use Raw Strings in Test Data
// Use template literals for multi-line test strings
const testData = `
Line 1
Line 2
Line 3
`;
// Avoid string concatenation with \n
const avoid = "Line 1\n" + "Line 2\n" + "Line 3";Conclusion
Understanding newline behavior in regular expressions is crucial for text processing tasks. Key takeaways:
- The
sflag makes.match newlines (dotAll mode) - The
mflag makes^and$match line boundaries (multiline mode) - These flags are independent and can be combined:
/pattern/sm - Use
[\s\S]or[^]for broad compatibility instead of.withsflag - Be aware of different newline sequences across operating systems
- Use non-greedy quantifiers to avoid matching too much
For testing and debugging regex patterns, especially those involving newlines, use the RegEx Tester to visualize matches and experiment with different flag combinations.