Regex Match a Line That Does NOT Contain a Word —Negative Lookahead Explained

Most regex patterns answer a simple question: does this text contain something?

But what if you need the opposite? Match everything that does NOT contain a specific word?

That is where negative lookahead comes in.

At first, this seems impossible. Regex searches for patterns —it cannot "not find" something by design. But with a zero-width assertion called negative lookahead, you can exclude patterns elegantly.

This article explains how negative lookahead works, when to use it, and why it often surprises developers who encounter it for the first time.

If you want to test these patterns interactively, the Regex Tester helps visualize lookahead behavior in real time.


What Is Negative Lookahead?

Negative lookahead is a zero-width assertion that checks whether a pattern does NOT appear ahead in the string.

The syntax is:

(?!pattern)

Key idea: it asserts that the text ahead does NOT match pattern, but it does NOT consume any characters.

That is the part developers usually find confusing.


How Negative Lookahead Differs From Normal Regex

Normal regex consumes characters as it matches.

foo

This matches f, o, o and advances the position.

Negative lookahead does NOT consume anything. It just looks ahead, checks, and if the condition is met, matching continues from the same position.

(?!foo)

This says: "from this position, foo must NOT be ahead." If that is true, matching continues. Nothing is consumed.


Basic Example: Matching Lines Without "ERROR"

Suppose you have log lines and want to find everything that is NOT an error:

Input:

INFO: server started
ERROR: database timeout
WARN: memory high
ERROR: connection lost

You want to match:

  • INFO: server started
  • WARN: memory high

Regex:

^(?!.*ERROR).+$

Explanation:

  • ^ -- start of line
  • (?!.*ERROR) -- at this position, assert there is no ERROR ahead
  • .+ -- match the line content
  • $ -- end of line

This is the most common use of negative lookahead.


JavaScript Example: Filtering Log Lines

const logs = [
  "INFO: server started",
  "ERROR: database timeout",
  "WARN: memory high",
  "ERROR: connection lost"
];

const regex = /^(?!.*ERROR).+$/;

const filtered = logs.filter(line => regex.test(line));

console.log(filtered);
// ["INFO: server started", "WARN: memory high"]

The pattern checks each line independently because we use anchors ^ and $.


Python Example: Filtering Lines

import re

logs = [
  "INFO: server started",
  "ERROR: database timeout",
  "WARN: memory high",
  "ERROR: connection lost"
]

regex = re.compile(r'^(?!.*ERROR).+$')

filtered = [line for line in logs if regex.match(line)]
print(filtered)
# ['INFO: server started', 'WARN: memory high']

Note that Python's re.match anchors at the start of the string by default, so ^ is optional here.


Negative Lookahead vs Positive Lookahead

The counterpart to negative lookahead is positive lookahead:

TypeSyntaxMeaning
Positive Lookahead(?=pattern)assert pattern IS ahead
Negative Lookahead(?!pattern)assert pattern is NOT ahead

Both are zero-width assertions. Neither consumes characters.


Example: Match Prices NOT Preceded by "Total"

Input:

Subtotal: $50.00
Tax: $5.00
Total: $55.00

You want to match dollar amounts that are NOT the total:

(?<!Total:\s)\$\d+\.\d{2}

But lookbehind has its own complexity. Alternatively, with negative lookahead at line level:

^(?!.*Total).*\$\d+\.\d{2}

This matches lines containing a price that do NOT contain "Total".


Common Mistake #1: Forgetting the Dot-Star

A frequent beginner mistake:

^(?!ERROR).+$

This only checks if the string starts with "ERROR". It does NOT check whether "ERROR" appears later.

The correct pattern:

^(?!.*ERROR).+$

The .* allows the lookahead to scan the entire line ahead.


Common Mistake #2: Lookahead in the Wrong Position

Negative lookahead must be placed BEFORE the consuming part of the pattern.

Wrong:

.+^(?!.*ERROR)

The lookahead runs at the position where it is placed. If you place it after consuming characters, the assertion may evaluate at the wrong position.


Common Mistake #3: Greedy Matching in Lookahead

Lookaheads are not immune to greedy behavior. The .* inside a lookahead can behave unexpectedly if you are not precise.

For example:

^(?!.*ERROR.*).+$

This is usually fine, but complex patterns with nested quantifiers inside lookaheads can lead to performance problems.

Related reading: Regex Greedy vs Lazy Matching Explained Simply


Real Use Case: Excluding Import Statements in Code Parsing

Suppose you are scanning a JavaScript codebase for function definitions but want to skip lines that are imports:

const code = `
import { foo } from 'bar';
const result = foo();
export function baz() {
  return result;
}
`;

const regex = /^(?!.*import).*function\s+\w+/gm;

const matches = code.match(regex);
console.log(matches);
// ["export function baz"]

The m flag makes ^ and $ match line boundaries. The negative lookahead skips any line containing import.


Real Use Case: Excluding Certain File Extensions

When listing files, you may want to exclude specific extensions:

import re

files = [
  "index.html",
  "style.css",
  "script.js",
  "readme.md",
  "package.json"
]

regex = re.compile(r'^(?!.*\.(html|css)$).+$')

filtered = [f for f in files if regex.match(f)]
print(filtered)
# ['script.js', 'readme.md', 'package.json']

This pattern excludes files ending in .html or .css.


Performance Considerations

Negative lookahead with .* inside can be slower than alternatives because the regex engine scans ahead across the entire line.

For simple exclusion:

^(?!.*ERROR).+$

This is generally fine. But nested lookaheads or lookaheads with complex alternations can create performance issues.

Tips:

  • keep lookahead patterns simple
  • avoid alternation inside lookaheads when possible
  • anchor with ^ to limit backtracking

Related reading: Regex Catastrophic Backtracking —How to Fix Regex That Freezes Your App


Negative Lookahead vs Using Two-Step Logic

Sometimes, the simplest solution is not a negative lookahead at all.

Instead of:

const regex = /^(?!.*ERROR).+$/;

You can write:

const lines = text.split("\n");
const filtered = lines.filter(line => !line.includes("ERROR"));

This is more readable, often faster, and does not require understanding lookahead.

But negative lookahead becomes essential when you need to exclude patterns within a single regex operation, especially during find-and-replace or complex matching.


Negative Lookahead in Find and Replace

One powerful use case: replace text that does NOT match a condition.

For example, wrap all URLs in anchor tags except those pointing to "example.com":

Visit https://google.com and https://example.com

Regex:

https:\/\/(?!example\.com)\S+

Replace with:

<a href="$&">$&</a>

Result:

Visit <a href="https://google.com">https://google.com</a> and https://example.com

The negative lookahead prevents matching example.com URLs.


Negative Lookahead in JavaScript Regex

JavaScript supports negative lookahead across all modern runtimes.

const regex = /^(?!.*ERROR).+$/m;

The m flag enables multiline mode, making ^ and $ match line boundaries.

Without the m flag, ^ matches only the start of the entire string.

Related reading: JavaScript Regex Flags Explained —g, i, m, s, u, y


Negative Lookahead in Python Regex

import re

pattern = re.compile(r'^(?!.*ERROR).+$', re.MULTILINE)

text = """INFO: started
ERROR: failed
WARN: high memory"""

for match in pattern.finditer(text):
    print(match.group())
# INFO: started
# WARN: high memory

Python uses re.MULTILINE (or re.M) for the same effect.


FAQ

What is negative lookahead in regex?

Negative lookahead (?!...) asserts that a pattern does NOT appear ahead in the string without consuming characters.


How do I match lines that do NOT contain a word?

Use:

^(?!.*word).+$

This checks each line from start to end.


What is the difference between lookahead and lookbehind?

Lookahead checks ahead. Lookbehind checks behind.

Negative lookahead: (?!...) Negative lookbehind: (?<!...)


Does negative lookahead consume characters?

No. Negative lookahead is a zero-width assertion. It checks a condition but does not advance the match position.


Why is my negative lookahead not working?

Common reasons:

  • missing .* before the excluded word inside the lookahead
  • lookahead placed at the wrong position
  • forgetting the m flag for multiline matching
  • greedy matching issues inside the lookahead

Is negative lookahead supported in all regex engines?

Most modern engines support it, but some older JavaScript environments may not.


When should I use negative lookahead vs string methods?

For simple exclusion, string methods like includes() are often more readable.

Use negative lookahead when:

  • you need a single regex operation
  • you are doing find-and-replace
  • you are in a context where string methods are not available

Final Thoughts

Negative lookahead is one of those regex features that seems confusing at first but becomes indispensable once you understand it.

The core insight is simple: a zero-width assertion checks a condition without consuming input. Once that clicks, patterns like (?!...) stop looking like cryptic punctuation and start looking like a logical tool.

Most real-world use cases boil down to a single pattern:

^(?!.*excluded).+$

That pattern solves an entire class of "match everything except" problems.

If you want to experiment with negative lookahead patterns interactively, the Regex Tester makes it much easier to see what the lookahead is actually doing.

You may also find these related developer tools useful: