How to Work with Raw Strings in Rust (r#""#)

Use raw string literals (prefixed with `r`) when you need to include many backslashes or quotes without escaping them, as the content between the delimiters is treated literally.

The backslash minefield

You're writing a regex to validate email addresses. You type ^[\w-]+. Then you need a literal dot, so you add \.. Suddenly you realize the backslash is escaping the dot for the regex engine, but Rust thinks the backslash is escaping the backslash. You type \\.. Now you have a string that looks like code golf gone wrong. Or you're copying a Windows file path from Explorer and pasting it into Rust, only to watch the compiler reject every backslash as an invalid escape sequence.

This is where raw strings save your sanity. They let you write text exactly as it should appear, without fighting the compiler's escape processor.

Raw strings skip the escape processor

Standard strings in Rust run through an escape processor before they become data. The compiler sees \n and turns it into a newline character. It sees \" and turns it into a quote. It sees \\ and turns it into a single backslash. This translation is essential for control characters, but it turns backslashes into a minefield whenever you're working with external syntax like regex, SQL, or file paths.

Raw strings skip the processor entirely. The compiler reads the characters between the delimiters and puts them straight into the string buffer. No translation. No interpretation. Just the bytes you typed.

Think of a standard string like a translator who interprets your instructions. You say "newline," and they type a newline. You say "backslash," and they type a backslash, but you have to shout "double backslash" to get one literal backslash. A raw string is like a courier who delivers the package exactly as packed. You put backslashes in, backslashes come out.

Minimal example

fn main() {
    // Standard string: backslash escapes the next character.
    // To get a literal backslash, you must double it.
    let path = "C:\\Users\\Dev\\notes.txt";

    // Raw string: backslash is just a backslash.
    // No doubling needed. The content is literal.
    let raw_path = r"C:\Users\Dev\notes.txt";

    // Both strings contain identical data at runtime.
    // The difference is only in how you write the source code.
    assert_eq!(path, raw_path);
}

The assert_eq! proves that both approaches produce the same result. The compiler generates identical machine code for both. Raw strings are purely a source-code readability feature. They make your intent match the text you're embedding.

Trust the raw string. If it looks right in the source, it's right in the buffer.

How the compiler counts hashes

Basic raw strings use r"...". The opening delimiter is r" and the closing delimiter is ". This works fine until your content contains a double quote. If you write r"{"key": "value"}", the compiler sees the quote after the brace and thinks the string ended. It rejects the code with an "unterminated raw string literal" error.

Rust solves this with hash marks. You can add # characters between the r and the quote to change the delimiter. The number of hashes must match exactly on both sides.

fn main() {
    // JSON contains quotes. Basic raw string fails here.
    // The compiler sees the quote inside and stops too early.
    // let bad_json = r"{"key": "value"}"; // Error: unterminated raw string

    // Use r#" to allow quotes inside.
    // The delimiter is now "# on both sides.
    let good_json = r#"{"key": "value", "nested": "test"}"#;

    println!("{}", good_json);
}

The lexer reads r, then counts the # characters. It stores that count. It then scans forward, character by character, looking for a quote followed by that exact number of hashes. This counting mechanism allows arbitrary nesting. If your content contains r#", you switch to r##"..."##. The lexer is looking for "##, so the r#" inside the content is safe.

There is no limit to the number of hashes. You can use r###"..."### or r########"..."######## if your content is pathological. In practice, r#"..."# covers 99% of cases. r##"..."## handles the rest.

Count your hashes. A mismatched delimiter swallows your code and produces confusing errors downstream.

Realistic examples: Regex and SQL

Regular expressions are the killer app for raw strings. Regex syntax relies heavily on backslashes for character classes, anchors, and escapes. Raw strings preserve the regex pattern exactly, making it readable and copy-pasteable from documentation or testing tools.

fn main() {
    // Regex for a simple email pattern.
    // Standard string requires escaping backslashes for character classes.
    // The pattern becomes hard to read and verify.
    let regex_standard = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";

    // Raw string preserves the regex syntax exactly.
    // The backslashes belong to the regex engine, not Rust.
    // You can copy this pattern directly from a regex tester.
    let regex_raw = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$";

    println!("Standard: {}", regex_standard);
    println!("Raw:      {}", regex_raw);
}

SQL queries benefit from raw strings too, especially multi-line queries. Raw strings allow literal newlines, which makes the query structure visible in the source code.

fn main() {
    // Multi-line SQL query using raw string.
    // Literal newlines improve readability.
    // Backticks and quotes in SQL don't interfere with the delimiter.
    let query = r#"
        SELECT users.id, users.name, orders.total
        FROM users
        JOIN orders ON users.id = orders.user_id
        WHERE orders.total > 100.00
        ORDER BY orders.created_at DESC
    "#;

    println!("{}", query);
}

Convention aside: cargo fmt preserves raw string formatting. It won't collapse your multi-line raw string into a single line. This makes raw strings safe for embedding large blocks of text. You can format your SQL or HTML exactly how you want, and the formatter respects it.

The no-escape trap

Raw strings do not process escape sequences. If you type \n in a raw string, you get two characters: a backslash and an 'n'. You do not get a newline. This catches developers who assume \n works everywhere.

fn main() {
    // Standard string: \n becomes a newline character.
    let with_newline = "Line 1\nLine 2";

    // Raw string: \n stays as backslash and n.
    // This is often not what you want for logs or output.
    let literal_backslash_n = r"Line 1\nLine 2";

    // Raw string with actual newline (press Enter here).
    // To get a newline in a raw string, you must type it literally.
    let raw_newline = r"Line 1
Line 2";

    assert_eq!(with_newline, raw_newline);
    assert_ne!(with_newline, literal_backslash_n);
}

If you need control characters like tabs or newlines generated by escape sequences, raw strings are the wrong tool. You must press Enter for newlines and Tab for tabs. This can clutter the source code for simple formatting. In those cases, standard strings are cleaner.

Raw strings are a readability tool. Use them to make the code match the intent, not to hide whitespace.

Pitfalls and compiler errors

The most common pitfall is mismatched hashes. If you open with r#", you must close with "#. If you accidentally close with ", the compiler keeps reading until it finds "#. This can swallow the rest of your file into the string, causing bizarre errors where functions or structs disappear.

The compiler rejects mismatched delimiters with an "unterminated raw string literal" error. It also rejects raw strings that contain the exact delimiter sequence without enough hashes. For example, r#"contains "# inside"# fails because the lexer sees "# inside the content and thinks the string ended. You must increase the hash count to r##"contains "# inside"##.

Another trap is assuming raw strings handle null bytes. They don't. Raw strings are still UTF-8 strings. If you need binary data with null bytes, use byte strings or byte raw strings (br"..."), though the latter is a separate feature. For text processing, raw strings are strictly for readability.

Don't fight the escape processor. Switch to raw strings when the text contains syntax that clashes with Rust's escape rules.

Decision matrix

Use raw strings for regular expressions when the pattern contains backslashes or character classes. The regex syntax stays readable, and you avoid doubling every backslash.

Use raw strings for Windows file paths when you copy-paste from the system. The path works immediately without manual escaping.

Use raw strings for JSON or HTML snippets when the content includes quotes. The r#"..."# syntax handles quotes naturally.

Use raw strings for SQL queries when embedding multi-line statements. The literal newlines improve readability, and backticks or special characters don't interfere.

Reach for standard strings when you need control characters like tabs or newlines generated by escape sequences. Raw strings require literal whitespace, which can clutter the source code for simple formatting.

Reach for standard strings when the content is short and contains no special characters. The overhead of raw string syntax isn't worth it for a simple label.

Where to go next