Back to Blog
C#

Using c# substring: Syntax, Errors, and Alternatives

Learn how to use the c# substring method correctly: syntax, boundary rules, common patterns, and allocation-aware alternatives for high-throughput code.

C# StringString ManipulationSubstring MethodReadOnlySpanString Performance
A visual metaphor showing a string being sliced into segments, with one segment highlighted to represent the substring extraction range in C#.

The c# substring operation is implemented by the string.Substring method, which has two overloads. The first takes a single start index and returns everything from that position to the end of the string. The second takes a start index and a length, returning exactly that many characters. Both overloads return a new string and leave the original unchanged.

The Substring Method's Contract

public string Substring(int startIndex) public string Substring(int startIndex, int length)

The one-argument overload is equivalent to calling the two-argument version with length equal to string.Length - startIndex. The method validates its arguments before doing any work: if startIndex is negative or greater than the string length, it throws ArgumentOutOfRangeException. The two-argument overload adds a second check: length must be non-negative, and startIndex + length must not exceed the string's length.

These checks happen before any characters are copied, so an invalid call never partially executes. The exception type is consistent across both overloads, which makes error handling straightforward: catch ArgumentOutOfRangeException only if you expect the input to be malformed, and prefer validating the input before calling the method.

What Happens at the Boundaries

A start index equal to the string's length is valid for both overloads. The one-argument version returns an empty string. The two-argument version requires length to be zero in that case; any positive length throws.

string text = "hello"; string all = text.Substring(0); // "hello" string empty = text.Substring(5); // "" string invalid = text.Substring(6); // ArgumentOutOfRangeException

The two-argument overload follows the same rule from the other side:

string middle = text.Substring(1, 3); // "ell" string end = text.Substring(3, 2); // "lo" string invalid = text.Substring(3, 3); // ArgumentOutOfRangeException

In the last example, startIndex + length equals 6, which exceeds the string length of 5. The boundary condition is startIndex + length <= text.Length, not startIndex < text.Length.

Extracting Fixed-Width Fields

A common real-world use is extracting fixed-width fields from structured text, such as log lines, fixed-format records, or padded data. When the format is stable, Substring with an explicit length is straightforward and readable.

string record = "2024-11-03 ERROR Connection refused"; string date = record.Substring(0, 10); string level = record.Substring(12, 5); string message = record.Substring(19);

The first call takes the date portion, the second takes the log level, and the third takes everything after the level. This pattern works well when the source format is guaranteed, but it breaks if a field is shorter than expected. A defensive approach is to check the string length before calling Substring, or to use IndexOf to locate delimiters dynamically.

Locating a Substring Dynamically

When the position of the text you need is not fixed, combine IndexOf with Substring to extract content between delimiters.

string url = "https://example.com/api/users/42"; int lastSlash = url.LastIndexOf('/'); string id = url.Substring(lastSlash + 1);

This extracts the trailing segment of a URL without knowing its length in advance. The same pattern works for extracting values between parentheses, quotes, or other delimiters. The key detail is that LastIndexOf returns the position of the delimiter, and adding one moves past it. If the delimiter is not found, LastIndexOf returns -1, so Substring(0) would return the entire string. Guard against that case when the delimiter is not guaranteed to exist.

Truncating Strings Safely

Truncation is another frequent use: limiting a display string to a maximum width. The naive approach throws when the input is shorter than the limit, so the length must be checked first.

public static string Truncate(string input, int maxLength) { if (input.Length <= maxLength) { return input; } return input.Substring(0, maxLength); }

This returns the original string when it already fits, and only allocates a new string when truncation is actually needed. The early return also avoids the ArgumentOutOfRangeException that would otherwise occur when maxLength exceeds the input length.

Allocation and Performance Characteristics

Every call to Substring allocates a new string. Strings are immutable in C#, so the returned string is a fresh object containing the extracted characters. In hot paths where strings are processed repeatedly, this allocation can add up.

Consider a loop that parses thousands of log lines per second. Each Substring call creates a new string that must later be garbage collected. The allocation cost is proportional to the extracted length, not the original length.

For high-throughput scenarios, ReadOnlySpan<char> provides a way to view a portion of a string without allocating. The AsSpan method returns a span over the original string's memory, and slicing that span avoids the new string allocation.

ReadOnlySpan<char> span = input.AsSpan(); ReadOnlySpan<char> date = span.Slice(0, 10); ReadOnlySpan<char> level = span.Slice(12, 5);

The Slice method does not copy characters; it produces a view over the same underlying memory. This is significantly cheaper when the extracted values are only needed temporarily, for example when comparing them or converting them to other types. If the extracted value must outlive the original string or be stored in a collection, converting the span back to a string via ToString() allocates exactly once, which is still better than multiple intermediate allocations.

Ranges as a Modern Alternative

C# 8 introduced range syntax that provides a more readable way to express substring operations in many cases.

string text = "hello world"; string first = text[..5]; // "hello" string last = text[6..]; // "world" string middle = text[1..4]; // "ell"

The range operator uses the same boundary rules as Substring, and it also throws ArgumentOutOfRangeException when the range is invalid. Ranges are syntactic sugar over the same underlying slicing behavior, so they do not avoid the allocation cost. They are most valuable when the intent is clearer with range syntax, such as taking a prefix or suffix. For dynamic lengths computed at runtime, Substring with explicit arguments is often clearer because the start and length are visible directly.

Choosing the Right Approach

The practical rule is to default to Substring for clarity, switch to range syntax when fixed boundaries make the intent more readable, and move to ReadOnlySpan slicing when profiling shows that allocation pressure matters. Each approach has the same boundary semantics, so the choice is about readability and allocation behavior rather than correctness.

Substring remains the right default for application-level code where the extracted value is stored, logged, or passed to another method. It is the most readable and the most familiar to other developers. Ranges are a good fit for fixed boundaries that read naturally as slices. Spans are the right tool when the extraction happens in a loop that processes large volumes of data and the extracted values are short-lived.