Updated on
Parse() and TryParse() both turn a string into a number. Parse() throws when the string is not one; TryParse() returns false and hands the value back through an out parameter.
That single difference decides which to use. A string we control is a bug when it fails to parse, so an exception is the right answer. A string from a user, a file, or an HTTP request fails all the time, and a bool is cheaper and clearer than a try/catch per row.
Every built-in numeric type has both, with the same overloads. So does bool, char, DateTime, Guid and more, and the shape never changes: pass a string, optionally a culture and a style, get a number back.
How Does the Parse() Method Work in C#?
Parse() converts a string into a number and throws when it cannot. There is no failure return value, so a call either produces a number or an exception.
Three exceptions cover the three ways a string can be wrong. ArgumentNullException means the input was null. FormatException means the characters do not form a number under the current rules. OverflowException means they do form a number, but one the target type cannot hold.
The overloads differ only in how much of “the current rules” we take control of. Parse(string) uses the current culture and the default style. Adding an IFormatProvider picks the culture, which decides the decimal separator and the sign symbols. Adding a NumberStyles value picks which characters are permitted at all: thousands separators, a decimal point, a currency symbol, a leading or trailing sign.
Because it throws, Parse() belongs where a bad string is a bug rather than an expectation.
- an
ArgumentNullException, if the input string isnull - a
FormatException, if the input string is of incorrect format - an
OverflowException, if the converted number exceeds the minimum or maximum range of the specified numeric type
That is also the line between this method and its neighbours: when the question is whether to use int.Parse() or Convert.ToInt32(), the answer turns on the same expectation about the input.
Parse(String)
We are most likely to encounter this overload of the Parse() method. It converts a string representation of a number to its numerical value.
For the Parse() method to work, we need to pass a valid string. A valid input string would be a sequence of digits from 0 to 9. Optionally, we can also have a leading and trailing whitespace along with a leading sign (+/-).
Let’s demonstrate this with an example of the Parse(String) method from the System.Int32 class:
Assert.AreEqual(456, int.Parse("456"));
Assert.AreEqual(-4567, int.Parse("-4567"));
Assert.Throws<FormatException>(() => int.Parse("3456.89"));
Assert.Throws<OverflowException>(() => int.Parse("34343454574745"));
Assert.Throws<ArgumentNullException>(() => int.Parse(null!));
Parse(String, IFormatProvider)
This overload of the Parse() method converts a string representation of a number that is in a culture-specific format to its numerical value.
Let’s consider a culture where we denote positive numbers by a leading # sign. Hence, in this culture, the number 1234 would become #1234. However, #1234 is not a valid number in other cultures and would fail to convert. In such a scenario, we specify the culture using Parse(String, IFormatProvider):
var culture = new CultureInfo("en-US");
culture.NumberFormat.PositiveSign = "#";
Assert.AreEqual(1234, int.Parse("#1234", culture));
Assert.Throws<FormatException>(() => int.Parse("#4567"));
Assert.Throws<FormatException>(() => int.Parse("$4561", culture));
Assert.Throws<OverflowException>(() => int.Parse("#34343454574745", culture));
A valid input string for Parse(String, IFormatProvider) is a sequence of digits from 0 to 9 with optional leading and trailing spaces along with a leading sign.
Parse(String, NumberStyles)
This overload of the Parse() method converts a string to its numerical value based on the specified style of the number.
We use the NumberStyles enum to specify the style elements such as separator symbols or exponential digits etc.
The combination of NumberStyles flags affect what a valid input string is. Along with the sequence of digits from 0 to 9, it may also contain leading and trailing signs, thousand operators, etc.:
Assert.AreEqual(3476, int.Parse("3476", NumberStyles.None));
Assert.AreEqual(76678, int.Parse("76678.0", NumberStyles.AllowDecimalPoint));
Assert.AreEqual(766780, int.Parse("766,780", NumberStyles.AllowThousands));
Assert.Throws<FormatException>(() => int.Parse("$45,618", NumberStyles.AllowThousands));
Assert.Throws<OverflowException>(() => int.Parse("56.89", NumberStyles.AllowDecimalPoint));
The last two lines are worth a second look. AllowDecimalPoint makes the decimal point legal to parse, so 56.89 is a well-formed number that simply cannot be held in an int. That is an OverflowException, not a FormatException, which is also why 76678.0 succeeds: its fractional part is zero.
Parse(String, NumberStyles, IFormatProvider)
This overload of the Parse() method combines the former two overloads. We use it to convert a number’s string representation in a culture-specific format to its numerical value based on a specified style:
Assert.AreEqual(78,
int.Parse("78,000",
NumberStyles.Float | NumberStyles.AllowThousands,
new CultureInfo("fr-FR")));
Assert.AreEqual(78000,
int.Parse("78,000",
NumberStyles.AllowThousands,
new CultureInfo("en-GB")));
Assert.AreEqual(78,
int.Parse("78.000",
NumberStyles.Float,
new CultureInfo("en-US")));
Assert.Throws<FormatException>(() =>
int.Parse("$78,000", NumberStyles.Float, new CultureInfo("en-US")));
Assert.Throws<OverflowException>(() =>
int.Parse("78.567", NumberStyles.AllowDecimalPoint, new CultureInfo("en-US")));
Parse(ReadOnlySpan<Char>, NumberStyles, IFormatProvider)
This overload takes a ReadOnlySpan<char> instead of a string, which lets us parse a slice of a larger buffer without allocating a substring. We use it to convert a span of characters in a culture-specific format to a numerical value based on a specified style:
Assert.AreEqual(78,
int.Parse("78,000".AsSpan(),
NumberStyles.Float | NumberStyles.AllowThousands,
new CultureInfo("fr-FR")));
Assert.AreEqual(78000,
int.Parse("78,000".AsSpan(),
NumberStyles.AllowThousands,
new CultureInfo("en-GB")));
Assert.AreEqual(78,
int.Parse("78.000".AsSpan(), NumberStyles.Float, new CultureInfo("en-US")));
Assert.Throws<FormatException>(() =>
int.Parse("$78,000".AsSpan(), NumberStyles.Float, new CultureInfo("en-US")));
Assert.Throws<OverflowException>(() =>
int.Parse("78.567".AsSpan(), NumberStyles.AllowDecimalPoint, new CultureInfo("en-US")));
Those are the overloads the method has had since .NET Core. .NET 7 and .NET 8 added more, which we cover below. Let’s turn our attention to the TryParse() method.
How Does the TryParse() Method Work in C#?
TryParse() answers the same question as Parse() and reports failure with a return value instead of an exception. It returns true on success and false on failure, and hands the number back through an out parameter.
The whole pattern fits on one line. if (int.TryParse(input, out var number)) gives us the test and the value together, and number stays in scope for the rest of the method.
On failure the out parameter is set to the type’s default, which is zero for the string overloads. That is a real value, not a marker, so 0 cannot tell us whether the string said zero or was not a number at all. The bool is the only reliable signal, and ignoring it is the mistake the method exists to prevent.
TryParse() also fails silently by design. It tells us that parsing did not work, never which of the three problems occurred.
The out parameter is the part worth getting comfortable with, so it helps to know how out parameters work in general. When the value itself is beside the point and we only care about the verdict, checking whether a string is a number at all is the narrower question.
TryParse(String, Int32)
This is the most commonly used overload of the TryParse() method. We use it to convert a number’s string representation to its numerical value.
The System.Int32 parameter contains the resulting numerical value if the conversion is successful or a zero in case of failure.
Let’s take the same example we did for Parse(String) and convert it to its TryParse() counterpart:
Assert.IsTrue(int.TryParse("45689", out int result));
Assert.AreEqual(45689, result);
Assert.IsFalse(int.TryParse("3456.89", out int formatNum));
Assert.IsFalse(int.TryParse("34343454574745", out int overflowNum));
Assert.IsFalse(int.TryParse((string?)null, out int nullNum));
So, with TryParse() instead of throwing exceptions or coding by exceptions, we can use the returned bool flag to control our code flow.
TryParse(String, NumberStyles, IFormatProvider, Int32)
This overload of TryParse() is similar to Parse(String, NumberStyles, IFormatProvider). We use it to convert a string representation of a number in a culture-specific format to its numerical value based on the specified style.
Let’s continue using our examples for Parse(String, NumberStyles, IFormatProvider) to understand the difference:
Assert.IsTrue(int.TryParse("78,000",
NumberStyles.Float | NumberStyles.AllowThousands,
new CultureInfo("fr-FR"),
out int frNum));
Assert.AreEqual(78, frNum);
Assert.IsTrue(int.TryParse("78,000",
NumberStyles.AllowThousands,
new CultureInfo("en-GB"),
out int gbNum));
Assert.AreEqual(78000, gbNum);
Assert.IsTrue(int.TryParse("78.000",
NumberStyles.Float,
new CultureInfo("en-US"), out int usNum));
Assert.AreEqual(78, usNum);
Assert.IsFalse(int.TryParse("$78,000",
NumberStyles.Float,
new CultureInfo("en-US"),
out int floatNum));
Assert.IsFalse(int.TryParse("78.567",
NumberStyles.AllowDecimalPoint,
new CultureInfo("en-US"),
out int decimalNum));
TryParse(ReadOnlySpan<Char>, Int32)
We use this overload of the TryParse() method to convert a number’s span representation to its numerical value.
This works similar to the TryParse(String, Int32) with the difference being ReadOnlySpan<Char> instead of String as the input parameter:
Assert.IsTrue(int.TryParse("45689".AsSpan(), out int result));
Assert.AreEqual(45689, result);
ReadOnlySpan<char> value = null;
Assert.IsFalse(int.TryParse(value, out int nullNum));
Assert.IsFalse(int.TryParse("3456.89".AsSpan(), out int formatNum));
Assert.IsFalse(int.TryParse("34343454574745".AsSpan(), out int overflowNum));
TryParse(ReadOnlySpan<Char>, NumberStyles, IFormatProvider, Int32)
This overload of TryParse() is similar to TryParse(String, NumberStyles, IFormatProvider, Int32). However, we pass ReadOnlySpan<Char> instead of a String here.
We use it to convert a span representation of a number in a specified style and culture-specific format to its numerical value.
Hence, we can simply convert the method used for TryParse(String, NumberStyles, IFormatProvider, Int32) like:
Assert.IsTrue(int.TryParse("78,000".AsSpan(),
NumberStyles.Float | NumberStyles.AllowThousands,
new CultureInfo("fr-FR"), out int frNum));
Assert.AreEqual(78, frNum);
Assert.IsTrue(int.TryParse("78,000".AsSpan(),
NumberStyles.AllowThousands,
new CultureInfo("en-GB"), out int gbNum));
Assert.AreEqual(78000, gbNum);
Assert.IsTrue(int.TryParse("78.000".AsSpan(),
NumberStyles.Float,
new CultureInfo("en-US"),
out int usNum));
Assert.AreEqual(78, usNum);
Assert.IsFalse(int.TryParse("$78,000".AsSpan(),
NumberStyles.Float,
new CultureInfo("en-US"),
out int floatNum));
Assert.IsFalse(int.TryParse("78.567".AsSpan(),
NumberStyles.AllowDecimalPoint,
new CultureInfo("en-US"),
out int decimalNum));
What Is the Difference Between Parse() and TryParse()?
Parse() throws when a string is not a number. TryParse() returns false. That is the entire difference, and it is enough to decide which one to call.
We reach for Parse() when a bad string means our program is wrong. A hard-coded constant, an application setting we ship, a value an earlier step already validated. The exception is the correct outcome, because there is no sensible next step anyway.
We reach for TryParse() when a bad string is ordinary. Form input, a CSV column, a query-string value, anything that crossed a network. Failure is expected traffic here, and a bool per row reads better than a try/catch per row.
The two are not different algorithms. Both accept and reject exactly the same strings under the same culture and style, so any string Parse() throws on is a string TryParse() returns false for.

The diagram makes the asymmetry visible: Parse() has two exits and one of them is an exception.
Parse() | TryParse() |
|
|---|---|---|
| Returns | the parsed number | bool (true when the conversion succeeded) |
| Where the value comes back | the return value | an out parameter |
Input is null | ArgumentNullException | false |
| Input is not a number | FormatException | false |
| Number is outside the type's range | OverflowException | false |
| Value after a failure | none, the call throws | default for the type (0 for every numeric type) |
| Tells us why it failed | yes, by exception type | no |
| Reach for it when | a bad string means our code is wrong | a bad string is normal traffic |
One row deserves a caveat the article used to state too strongly. TryParse() wins on strings that fail, because throwing and catching an exception is expensive and returning false is not. On strings that parse, treat them as equals until a benchmark of our own says otherwise. And once we have the number, the format strings that turn a number back into a string handle the trip in the other direction.
Which Overloads Did .NET 7 and .NET 8 Add?
Since .NET 7, Parse() and TryParse() are interface members rather than loose static methods. Every numeric type implements IParsable<T> and ISpanParsable<T>, both of which require an IFormatProvider. That is where int.TryParse(s, provider, out var n) comes from: a culture-aware overload with no NumberStyles argument to supply.
The point of the change is generic code. A method declared where T : IParsable<T> can call T.Parse(s, provider) for any type that implements it, which static abstract interface members made possible and nothing before them did.
.NET 8 added a UTF-8 pair through IUtf8SpanParsable<T>. int.TryParse(ReadOnlySpan<byte> utf8Text, out int result) lets us read a number straight out of a UTF-8 buffer without decoding it into a string first.
The additions have one visible cost. int.TryParse(null, out var n) no longer compiles, because null now fits both the string and the UTF-8 overload, so it needs a cast: (string?)null.
Int32 exposes nine TryParse() overloads today. Four of them predate .NET 7.
Otherwise the additions are purely additive, and nothing that compiled before .NET 7 stopped compiling, so there is no migration here to do.
Which .NET Types Have Parse() and TryParse()?
Every built-in numeric type carries both methods with the same overload set: byte, sbyte, short, ushort, int, uint, long, ulong, float, double and decimal.
They are not numeric-only. bool, char, DateTime, DateTimeOffset, TimeSpan, Guid, Version and IPAddress all expose TryParse(), and Enum.TryParse<TEnum>() does the same job for enum members.
The shape transfers, but the rules do not. bool.TryParse() accepts only "True" and "False", case-insensitively, so "1" and "yes" both return false. DateTime.TryParse() reads the current culture’s date order, which makes 03/04/2026 March in en-US and April in en-GB.
Enum.TryParse<TEnum>() is the one to watch. It returns true for any number inside the underlying type’s range, even when no member of the enum has that value, which is the reason Enum.IsDefined() exists at all.
So the method name is the easy part. What each type counts as a valid string is where the surprises live, and it is why passing an explicit IFormatProvider matters more than it looks.
All the other numeric types have their respective Parse() and TryParse() methods with overloads similar to System.Int32.
So instead of an int.Parse(), we would use long.Parse(), double.Parse(), or a decimal.Parse() depending on whether the string representation (or span representation when applicable) is of a System.Int64, System.Double, or a System.Decimal number respectively.
The same is applicable for TryParse with long.TryParse(), double.TryParse(), decimal.TryParse() etc.
| To parse a… | Call | Watch out for |
|---|---|---|
int | int.TryParse(s, out var n) | |
long | long.TryParse(s, out var n) | The type to reach for when int overflows |
short, byte | short.TryParse, byte.TryParse | Narrow ranges, so overflow is common |
uint, ulong, ushort, sbyte | uint.TryParse and siblings | A leading - makes the unsigned ones fail |
float, double | double.TryParse(s, out var d) | Also accepts NaN and Infinity |
decimal | decimal.TryParse(s, out var d) | The one to use for money |
bool | bool.TryParse(s, out var b) | Only "True" and "False", case-insensitively; surrounding whitespace is trimmed |
char | char.TryParse(s, out var c) | The string must be exactly one character |
DateTime | DateTime.TryParse(s, out var dt) | The current culture decides the date order |
DateTimeOffset | DateTimeOffset.TryParse(s, out var dto) | Keeps the UTC offset, which DateTime drops |
TimeSpan | TimeSpan.TryParse(s, out var ts) | |
Guid | Guid.TryParse(s, out var g) | Accepts several brace and hyphen forms |
| an enum member | Enum.TryParse<TEnum>(s, out var e) | Succeeds for any number in the underlying range |
Version | Version.TryParse(s, out var v) | |
IPAddress | IPAddress.TryParse(s, out var ip) | System.Net, not System |
The int row is the one most readers arrive on, and it is often only half the job: the full walkthrough of converting a string to an int covers the surrounding decisions. Two other rows have articles of their own, since converting a string to a DateTime and converting a string to an enum member both carry rules the table can only summarise.
Conclusion
In the article, we learned about how the Parse and TryParse in C# work and their different overloads.
Parse() is useful in the scenarios where we care about the type of exception that can occur during failure to convert a string to its numerical value. Whereas, with TryParse() we get a better alternative in terms of reliability, and a cheaper one whenever failure is expected, by not having to deal with exception handling.
Hence, in cases where we don’t need the exact details of the exceptions, it’s almost always better to go with TryParse().
Tested with .NET 10.0.302.
