Regex #

A regular expression (regex) is a mini-language for describing patterns in text — an irreplaceable tool for validation, extraction, and string transformation. Dart implements regex through the RegExp class, which follows the ECMAScript standard (the same as JavaScript), so patterns you write in an online regex tester will run identically in Dart. Although powerful, regex also has a reputation as code that’s “written once, never understood again” — this article builds understanding from the basics up to fairly advanced patterns, along with when you should not use regex.

RegExp and Raw Strings #

RegExp is the class representing a regular expression. Because regex patterns use many backslashes (\) which are also escape characters in Dart, almost always use a raw string (r'...') to write patterns — this eliminates ambiguity:

// Without a raw string — backslashes must be escaped twice
RegExp tanpaRaw = RegExp('\\\\d+');     // \\ so Dart produces a literal \

// With a raw string — backslashes are passed directly to the regex engine
RegExp denganRaw = RegExp(r'\\d+');    // ✓ cleaner and unambiguous

// For complex patterns, the difference is striking
RegExp emailBuruk = RegExp('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}\\$');
RegExp emailBaik = RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$');

RegExp Flags #

RegExp supports four optional flags that change how a pattern is interpreted:

// caseSensitive — default: true (uppercase/lowercase distinguished)
RegExp sensitif = RegExp(r'dart');
RegExp tidakSensitif = RegExp(r'dart', caseSensitive: false);

print(sensitif.hasMatch('Dart'));        // false
print(tidakSensitif.hasMatch('DART'));   // true
print(tidakSensitif.hasMatch('dart'));   // true

// multiLine — ^ and $ match the start/end of EVERY line
String teks = 'baris pertama\\nbaris kedua\\nbaris ketiga';
RegExp single = RegExp(r'^baris');
RegExp multi = RegExp(r'^baris', multiLine: true);

print(single.allMatches(teks).length);  // 1 — only the string start
print(multi.allMatches(teks).length);   // 3 — the start of every line

// dotAll — . matches ANY character including newline
String multiline = 'awal\\nakhir';
RegExp dotNormal = RegExp(r'awal.akhir');
RegExp dotAll = RegExp(r'awal.akhir', dotAll: true);

print(dotNormal.hasMatch(multiline));   // false — . doesn't match \\n
print(dotAll.hasMatch(multiline));      // true — . matches \\n

// unicode — enable Unicode mode for characters outside the BMP
RegExp unicode = RegExp(r'\\p{L}+', unicode: true); // matches all Unicode letters

RegExp Methods — Working with Patterns #

RegExp pola = RegExp(r'\\d{3,}');
String teks = 'Ada 123 apel, 45 jeruk, dan 6789 mangga';

// hasMatch — check whether there's a match (most efficient for yes/no checks)
bool ada = pola.hasMatch(teks);        // true

// firstMatch — get details of the first match
RegExpMatch? pertama = pola.firstMatch(teks);
print(pertama?.group(0));              // '123'
print(pertama?.start);                 // 4 (starting position in the string)
print(pertama?.end);                   // 7 (ending position, exclusive)

// stringMatch — the string of the first match (shortcut)
String? str = pola.stringMatch(teks);  // '123'

// allMatches — all matches as an Iterable
Iterable<RegExpMatch> semua = pola.allMatches(teks);
for (final m in semua) {
  print('Cocok: \"${m.group(0)}\" di posisi ${m.start}-${m.end}');
}
// Cocok: \"123\" di posisi 4-7
// Cocok: \"6789\" di posisi 30-34

// allMatches with start — begin searching from a specific position
Iterable<RegExpMatch> dariPosisi = pola.allMatches(teks, 10);
// search begins at index 10

Full Regex Pattern Syntax #

Characters and Character Classes #

.       any character except newline (unless dotAll is active)
\\d      digit [0-9]
\\D      not a digit
\\w      word character [a-zA-Z0-9_]
\\W      not a word character
\\s      whitespace (space, tab, newline, carriage return)
\\S      not whitespace

[abc]   one of: a, b, or c
[^abc]  not a, b, or c
[a-z]   lowercase letters a through z
[A-Z]   uppercase letters A through Z
[0-9]   digits 0 through 9
[a-zA-Z0-9]  all letters and digits

Special characters that need escaping: . * + ? ^ $ { } [ ] | ( ) \\
// Character class examples
RegExp huruf = RegExp(r'[a-zA-Z]+');
RegExp bukanHuruf = RegExp(r'[^a-zA-Z]+');
RegExp hexColor = RegExp(r'#[0-9a-fA-F]{6}');

print(huruf.stringMatch('abc123')); // 'abc'
print(hexColor.hasMatch('#FF5733')); // true
print(hexColor.hasMatch('#GG0000')); // false — G isn't hex

Anchors — Positions in a String #

^   start of string (or start of line if multiLine is active)
$   end of string (or end of line if multiLine is active)
\\b  word boundary — between \\w and \\W
\\B  not a word boundary
// ^ and $ ensure the pattern matches the ENTIRE string
RegExp hanyaAngka = RegExp(r'^\\d+$');

print(hanyaAngka.hasMatch('12345'));    // true
print(hanyaAngka.hasMatch('123abc'));   // false — there are letters

// \\b for word boundaries — ensure a whole-word match
RegExp kataDart = RegExp(r'\\bDart\\b');

print(kataDart.hasMatch('Dart adalah bahasa'));  // true
print(kataDart.hasMatch('Dartmouth'));           // false — 'Dart' isn't a whole word
print(kataDart.hasMatch('DartPad'));             // false

Quantifiers — Repetition Counts #

*       0 or more (greedy)
+       1 or more (greedy)
?       0 or 1 (greedy)
{n}     exactly n times
{n,}    n or more times
{n,m}   between n and m times (inclusive)

*?      0 or more (lazy/non-greedy)
+?      1 or more (lazy/non-greedy)
??      0 or 1 (lazy/non-greedy)
{n,m}?  between n and m times (lazy)

Greedy vs Lazy #

Greedy quantifiers take as many matching characters as possible. Lazy quantifiers (add ?) take as few as possible:

String html = '<b>Teks tebal</b> dan <i>Italic</i>';

// Greedy — takes from <b> all the way to </i> (too much!)
RegExp greedyTag = RegExp(r'<.+>');
print(greedyTag.stringMatch(html));
// '<b>Teks tebal</b> dan <i>Italic</i>' — the entire string!

// Lazy — takes the shortest matching tag
RegExp lazyTag = RegExp(r'<.+?>');
for (final m in lazyTag.allMatches(html)) {
  print(m.group(0)); // '<b>', '</b>', '<i>', '</i>' — each tag
}
// ANTI-PATTERN: accidental greediness produces overly broad matches
String json = '{\"nama\": \"Budi\", \"kota\": \"Jakarta\"}';
RegExp kunciGreedy = RegExp(r'\".*\"');
print(kunciGreedy.stringMatch(json));
// '\"nama\": \"Budi\", \"kota\": \"Jakarta\"' — takes everything!

// CORRECT: lazy to match JSON strings properly
RegExp kunciLazy = RegExp(r'\".*?\"');
for (final m in kunciLazy.allMatches(json)) {
  print(m.group(0)); // '\"nama\"', '\"Budi\"', '\"kota\"', '\"Jakarta\"'
}

Capturing Groups #

Groups () capture part of the match and make it accessible separately from the entire match:

// Groups are indexed starting from 1
// group(0) = the entire match
// group(1) = the first group, etc

RegExp tanggal = RegExp(r'(\\d{4})-(\\d{2})-(\\d{2})');
String teks = 'Tanggal lahir: 1998-05-20';

RegExpMatch? match = tanggal.firstMatch(teks);
if (match != null) {
  print(match.group(0));  // '1998-05-20' — the entire match
  print(match.group(1));  // '1998' — year
  print(match.group(2));  // '05'   — month
  print(match.group(3));  // '20'   — day
}

// Named groups — more expressive for complex patterns
RegExp tanggalNamed = RegExp(r'(?<tahun>\\d{4})-(?<bulan>\\d{2})-(?<hari>\\d{2})');
RegExpMatch? namedMatch = tanggalNamed.firstMatch('2024-11-15');
if (namedMatch != null) {
  print(namedMatch.namedGroup('tahun'));  // '2024'
  print(namedMatch.namedGroup('bulan'));  // '11'
  print(namedMatch.namedGroup('hari'));   // '15'
}

Non-Capturing Groups (?:...) #

Use (?:...) when you need a group for logic but don’t need to capture its result:

// Alternation without capturing the group
RegExp protokol = RegExp(r'(?:https?|ftp)://[\\w./-]+');
print(protokol.hasMatch('https://dart.dev'));  // true
print(protokol.hasMatch('ftp://files.dart'));  // true

// Group 1 won't contain the protocol because it's non-capturing
RegExpMatch? m = protokol.firstMatch('https://dart.dev');
print(m?.group(1));  // null — there's no capturing group

Lookahead and Lookbehind #

Lookahead and lookbehind match positions based on context without consuming characters:

// Positive lookahead (?=...) — matches if FOLLOWED by the pattern
RegExp hargaRupiah = RegExp(r'\\d+(?=\\s?Rb)');
String teks = '500 Rb dan 1500 Rb dan 200';
for (final m in hargaRupiah.allMatches(teks)) {
  print(m.group(0)); // '500', '1500' — only numbers before 'Rb'
}

// Negative lookahead (?!...) — matches if NOT followed by the pattern
RegExp angkaTanpaRb = RegExp(r'\\d+(?!\\s?Rb|\\d)');
for (final m in angkaTanpaRb.allMatches(teks)) {
  print(m.group(0)); // '200' — numbers not followed by 'Rb'
}

// Positive lookbehind (?<=...) — matches if PRECEDED by the pattern
RegExp setelahRp = RegExp(r'(?<=Rp\\s?)\\d+');
print(setelahRp.stringMatch('Harga: Rp 50000')); // '50000'

// Negative lookbehind (?<!...) — matches if NOT preceded by the pattern
RegExp bukanSetelahRp = RegExp(r'(?<!Rp\\s?)\\d+');
// numbers that aren't Rupiah prices

Replacing Text #

replaceAll and replaceFirst #

String teks = 'Saya suka jeruk, jeruk sangat manis';

// Replace all matches with a fixed string
String diganti = teks.replaceAll(RegExp(r'jeruk'), 'mangga');
print(diganti); // 'Saya suka mangga, mangga sangat manis'

// Replace only the first match
String pertama = teks.replaceFirst(RegExp(r'jeruk'), 'mangga');
print(pertama); // 'Saya suka mangga, jeruk sangat manis'

// Use a backreference \\1 in the replacement string
String camel = 'namaDepan belakangNama';
String kebab = camel.replaceAllMapped(
  RegExp(r'([A-Z])'),
  (m) => '-${m.group(0)!.toLowerCase()}',
);
print(kebab); // 'nama-depan belakang-nama' — camelCase to kebab-case

replaceAllMapped — Dynamic Transformations #

replaceAllMapped allows replacements based on the match content — far more powerful than a static replacement string:

// Format numbers with thousands separators
String formatRibuan(String angka) {
  return angka.replaceAllMapped(
    RegExp(r'(\\d{1,3})(?=(\\d{3})+(?!\\d))'),
    (m) => '${m.group(1)}.',
  );
}
print(formatRibuan('1234567'));  // '1.234.567'
print(formatRibuan('9999'));     // '9.999'

// Turn all URLs into HTML links
String tambahLink(String teks) {
  return teks.replaceAllMapped(
    RegExp(r'https?://[^\\s]+'),
    (m) => '<a href=\"${m.group(0)}\">${m.group(0)}</a>',
  );
}
print(tambahLink('Kunjungi https://dart.dev untuk info lebih lanjut'));
// 'Kunjungi <a href=\"https://dart.dev\">https://dart.dev</a> untuk info lebih lanjut'

// Replace template variables {nama} with values from a Map
String isiTemplate(String template, Map<String, String> data) {
  return template.replaceAllMapped(
    RegExp(r'\\{(\\w+)\\}'),
    (m) => data[m.group(1)] ?? m.group(0)!,
  );
}

String pesan = isiTemplate(
  'Halo {nama}, pesananmu #{id} sudah dikirim ke {alamat}.',
  {'nama': 'Budi', 'id': 'ORD-001', 'alamat': 'Jakarta'},
);
print(pesan); // 'Halo Budi, pesananmu #ORD-001 sudah dikirim ke Jakarta.'

Making RegExp a Constant #

If a regex is used repeatedly, declare it as a constant — avoiding recompiling the pattern every time it’s used:

// ANTI-PATTERN: creating a new RegExp on every function call
bool isEmail(String input) {
  return RegExp(r'^[\\w\\.-]+@[\\w\\.-]+\\.\\w{2,}$').hasMatch(input); // compiled every call
}

// CORRECT: declare once as const or static
class Validator {
  static final RegExp _email = RegExp(r'^[\\w\\.-]+@[\\w\\.-]+\\.\\w{2,}$');
  static final RegExp _phone = RegExp(r'^\\+?[\\d\\s\\-\\(\\)]{10,}$');
  static final RegExp _url   = RegExp(r'^https?://[\\w\\.-]+(?:/[^\\s]*)?$');

  static bool isEmail(String s) => _email.hasMatch(s);
  static bool isPhone(String s) => _phone.hasMatch(s);
  static bool isUrl(String s)   => _url.hasMatch(s);
}

Real-World Use Cases #

Format Validation #

abstract class Validator {
  // Email — simple but covers common cases
  static final RegExp email = RegExp(
    r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
  );

  // Indonesian phone numbers (08xx, +628xx, 628xx)
  static final RegExp phoneId = RegExp(
    r'^(\\+62|62|0)8[1-9][0-9]{6,9}$',
  );

  // Password at least 8 characters, with uppercase, lowercase, and digits
  static final RegExp passwordKuat = RegExp(
    r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$',
  );

  // Indonesian postal code (5 digits)
  static final RegExp kodePos = RegExp(r'^\\d{5}$');

  // NIK (16 digits)
  static final RegExp nik = RegExp(r'^\\d{16}$');

  // URL-friendly slug
  static final RegExp slug = RegExp(r'^[a-z0-9]+(?:-[a-z0-9]+)*$');

  // Hex color
  static final RegExp hexColor = RegExp(r'^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$');
}

// Usage
void validasiForm(String email, String phone, String password) {
  if (!Validator.email.hasMatch(email)) {
    throw ArgumentError('Invalid email: $email');
  }
  if (!Validator.phoneId.hasMatch(phone)) {
    throw ArgumentError('Invalid phone number: $phone');
  }
  if (!Validator.passwordKuat.hasMatch(password)) {
    throw ArgumentError('Password too weak');
  }
}

Data Extraction #

// Extract all URLs from text
List<String> ekstrakUrl(String teks) {
  final pola = RegExp(r'https?://[^\\s<>\"{}|\\\\^`\\[\\]]+');
  return pola.allMatches(teks).map((m) => m.group(0)!).toList();
}

// Extract all hashtags from text
List<String> ekstrakHashtag(String teks) {
  final pola = RegExp(r'#\\w+');
  return pola.allMatches(teks).map((m) => m.group(0)!).toList();
}
print(ekstrakHashtag('Belajar #dart dan #flutter hari ini!'));
// ['#dart', '#flutter']

// Parse a log format: [LEVEL] timestamp - message
void parseLog(String baris) {
  final pola = RegExp(
    r'^\\[(\\w+)\\]\\s(\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}:\\d{2})\\s-\\s(.+)$',
  );
  final m = pola.firstMatch(baris);
  if (m != null) {
    print('Level: ${m.group(1)}');
    print('Waktu: ${m.group(2)}');
    print('Pesan: ${m.group(3)}');
  }
}
parseLog('[ERROR] 2024-11-15 14:30:45 - Koneksi database gagal');

// Extract numbers from mixed text
List<double> ekstrakAngka(String teks) {
  final pola = RegExp(r'-?\\d+(?:\\.\\d+)?');
  return pola.allMatches(teks)
      .map((m) => double.parse(m.group(0)!))
      .toList();
}
print(ekstrakAngka('Suhu: -5.5 derajat, kelembaban: 80%'));
// [-5.5, 80.0]

String Transformations #

// camelCase to snake_case
String camelToSnake(String s) {
  return s
      .replaceAllMapped(
        RegExp(r'[A-Z]'),
        (m) => '_${m.group(0)!.toLowerCase()}',
      )
      .replaceFirst(RegExp(r'^_'), ''); // remove the leading underscore
}
print(camelToSnake('namaLengkapPengguna')); // 'nama_lengkap_pengguna'

// Normalize whitespace — replace multiple spaces/newlines with a single space
String normalWhitespace(String s) {
  return s.trim().replaceAll(RegExp(r'\\s+'), ' ');
}
print(normalWhitespace('  teks   dengan   banyak   spasi  '));
// 'teks dengan banyak spasi'

// Remove non-alphanumeric characters
String hanyaAlphanumeric(String s) {
  return s.replaceAll(RegExp(r'[^\\w\\s]'), '');
}
print(hanyaAlphanumeric('Halo, Dunia! #Dart2024'));
// 'Halo Dunia Dart2024'

When Not to Use Regex #

Regex is the right tool for many cases, but there are situations where it isn’t the best choice:

USE regex when:
  ✓ Validating formats (email, phone, postal code, slug)
  ✓ Extracting parts of text with well-defined patterns
  ✓ Doing find-and-replace with conditional logic
  ✓ Processing logs or simple structured text formats

DON'T use regex when:
  ✗ Parsing HTML or XML — use a proper parser (the html package)
  ✗ Parsing JSON — use jsonDecode
  ✗ Validating complex URLs — Uri.tryParse is more reliable
  ✗ Simple string operations that could use contains/split/substring
  ✗ Very long and complex patterns — consider a dedicated parser
// ANTI-PATTERN: regex for parsing HTML
RegExp htmlTag = RegExp(r'<a[^>]*href=[\"\\']([^\"\\']+)[\"\\'][^>]*>([^<]+)</a>');
// ✗ Doesn't handle nested tags, HTML entities, different attribute orders, etc.

// CORRECT: use the html parser package
import 'package:html/parser.dart' as html;
final document = html.parse(htmlString);
final links = document.querySelectorAll('a');

// ANTI-PATTERN: regex for full URL validation
RegExp urlKompleks = RegExp(r'^(https?://)?(www\\.)?...');
// ✗ URLs are very complex — too many edge cases

// CORRECT: use Uri.tryParse
bool isValidUrl(String s) {
  final uri = Uri.tryParse(s);
  return uri != null && (uri.isScheme('http') || uri.isScheme('https'));
}

Regex Anti-Patterns #

Catastrophic Backtracking #

Patterns with nested quantifiers and alternation can make the regex engine take exponential time for certain inputs:

// ANTI-PATTERN: nested quantifiers vulnerable to catastrophic backtracking
RegExp berbahaya = RegExp(r'^(a+)+$');
// For input 'aaaaaaaaaaaab', the regex engine tries every combination
// of a+ groupings before giving up — can hang for seconds/minutes!

// CORRECT: avoid nested quantifiers with an equivalent pattern
RegExp aman = RegExp(r'^a+$');
// Same effect without the catastrophic backtracking risk

Over-relying on Regex for Business Validation #

// ANTI-PATTERN: complex business validation with one giant regex
// An RFC 5321 "valid" email regex is hundreds of characters long
// and still isn't perfect

// CORRECT: combine a regex for the basic format with separate business validation
bool isValidEmail(String email) {
  // Regex only for the basic format
  if (!RegExp(r'^[\\w\\.-]+@[\\w\\.-]+\\.\\w{2,}$').hasMatch(email)) return false;

  // Additional business validation (not possible with regex)
  final parts = email.split('@');
  if (parts[0].length > 64) return false;     // local part max 64 chars
  if (parts[1].length > 255) return false;    // domain max 255 chars
  if (email.length > 320) return false;       // total max 320 chars

  return true;
}

Summary #

  • Always use raw strings (r'...') for regex patterns — avoids backslash conflicts between Dart escapes and regex escapes.
  • Important flags: caseSensitive: false for case-insensitive search, multiLine: true so ^ and $ match every line, dotAll: true so . matches including newlines.
  • hasMatch for yes/no checks (most efficient), firstMatch for first-match details, allMatches for all matches.
  • Greedy vs lazy — greedy quantifiers (*, +) take as much as possible; add ? (*?, +?) for lazy, which takes as little as possible. Use lazy for HTML-like parsing.
  • Capturing groups () to extract specific parts of a match. Use (?:...) for non-capturing groups when you don’t need to access the content.
  • Named groups (?<nama>...) make patterns more readable and namedGroup('nama') access is more expressive than numeric indices.
  • replaceAllMapped for dynamic transformations where the replacement depends on the match content — far more powerful than replaceAll with a static string.
  • Declare RegExp as static final in a class or as a top-level constant — avoid creating new objects every time a function is called.
  • Don’t use regex for HTML/XML — nested structures can’t be described with a regular language. Use a proper parser.
  • Avoid nested quantifiers like (a+)+ — vulnerable to catastrophic backtracking that can hang the program for certain inputs.

← Previous: Date & Time   Next: Pub.dev →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact