Throwing Exceptions in C#
Vaibhav • September 10, 2025
In the previous article, we explored the rich set of properties that every exception object carries - from Message
and StackTrace
to InnerException
and TargetSite
. These properties
help us understand what went wrong and where. But before any of that information becomes useful, an exception
must be thrown. In this article, we’ll learn how to throw exceptions in C#, when to do it, and
how to do it responsibly.
What Does “Throwing” Mean?
Throwing an exception means deliberately signaling that something has gone wrong. It’s like raising your hand in
class to say, “I can’t continue - something’s broken.” In C#, you throw an exception using the throw
keyword followed by an exception object.
throw new InvalidOperationException("Operation is not allowed in this state.");
This line creates a new InvalidOperationException
and throws it immediately.
The runtime stops executing the current method and begins searching for a matching catch
block up the call stack.
Throwing Built-in Exceptions
C# provides a rich set of built-in exception types for common error conditions. You should use these whenever they accurately describe the problem. Here are a few examples:
if (input == null)
throw new ArgumentNullException(nameof(input));
if (index < 0 || index >= array.Length)
throw new IndexOutOfRangeException("Index is outside the bounds of the array.");
if (!isConnected)
throw new InvalidOperationException("Cannot perform operation while disconnected.");
These exceptions are well-known, well-documented, and expected by other developers. They also integrate nicely with tools like Visual Studio and debuggers.
Prefer built-in exceptions when they clearly describe the error. Don’t reinvent the wheel with custom types unless the error is domain-specific.
Throwing Custom Exceptions
In the article on custom exceptions, we learned how to define our own exception types. Throwing them is just as straightforward:
if (balance < withdrawalAmount)
throw new InsufficientFundsException("Not enough balance to complete the transaction.");
This makes your code more expressive and easier to understand. It also allows consumers of your API to catch specific errors and respond appropriately.
Throwing with Context
A good exception includes a clear, actionable message. Avoid vague messages like “Something went wrong.” Instead, describe what failed and why:
throw new ArgumentException("Username must be at least 3 characters long.", nameof(username));
This message tells the developer exactly what the problem is and which parameter caused it. The nameof
operator ensures the parameter name stays in sync with the code.
Exception messages are for developers, not end users. They should be clear, technical, and precise - not friendly or vague.
Throwing with Inner Exceptions
When one error causes another, you can preserve the original exception using the InnerException
property. This is called exception chaining:
try
{
ConnectToDatabase();
}
catch (SqlException ex)
{
throw new DataAccessException("Failed to connect to database.", ex);
}
This pattern lets you wrap low-level errors in higher-level context, while still preserving the original stack trace and message.
Throwing vs Returning Error Codes
In some languages, errors are signaled using return values (e.g., -1
or null
). C# favors exceptions for unexpected or invalid states. Use return values
for expected outcomes (e.g., “not found”), and exceptions for truly exceptional conditions.
// Expected outcome: item not found
Item item = FindItem(id);
if (item == null)
return null;
// Unexpected outcome: corrupted data
if (item.IsCorrupted)
throw new DataCorruptionException("Item data is invalid.");
This distinction helps keep your code clean and predictable.
Throwing in Property Setters
You can throw exceptions in property setters to enforce validation rules:
private string _email;
public string Email
{
get => _email;
set
{
if (!IsValidEmail(value))
throw new ArgumentException("Email format is invalid.", nameof(value));
_email = value;
}
}
This ensures that your object stays in a valid state, even when used by other code.
Throwing in Constructors
Constructors can throw exceptions if the object cannot be created in a valid state:
public User(string username)
{
if (string.IsNullOrWhiteSpace(username))
throw new ArgumentException("Username cannot be empty.", nameof(username));
Username = username;
}
This prevents invalid objects from entering your system and causing bugs later.
Throwing in Async Methods
In asynchronous methods, exceptions are thrown just like in synchronous code. They are captured in the returned
Task
and rethrown when awaited:
async Task LoadDataAsync()
{
if (!File.Exists("data.txt"))
throw new FileNotFoundException("Data file is missing.");
string content = await File.ReadAllTextAsync("data.txt");
}
When you await
this method, the exception will propagate normally:
try
{
await LoadDataAsync();
}
catch (FileNotFoundException ex)
{
Console.WriteLine("Error: " + ex.Message);
}
If you forget to await
a task, its exception may go
unnoticed. Always await tasks or inspect their Exception
property.
Throwing with Performance in Mind
Exceptions are relatively expensive compared to regular control flow. Don’t use them for routine logic. For example, avoid this:
// ❌ Bad: using exceptions for control flow
try
{
int value = int.Parse(input);
}
catch
{
value = 0;
}
Instead, use TryParse
:
// ✅ Good: using TryParse
if (!int.TryParse(input, out int value))
value = 0;
This avoids the overhead of exception handling and keeps your code clean.
Common Mistakes to Avoid
Throwing exceptions is powerful, but it can be misused. Here are some pitfalls to watch out for:
- Throwing
new Exception()
- use specific types instead. - Throwing without a message - always include a clear description.
- Swallowing exceptions - don’t catch and ignore without logging or rethrowing.
- Using exceptions for control flow - prefer
TryParse
,TryGetValue
, etc. - Throwing from
finally
blocks - this can hide other exceptions.
Exceptions should be rare, meaningful, and informative. Don’t throw casually - treat them as part of your API contract.
Summary
Throwing exceptions is how you signal errors in C#. It’s a deliberate act that stops normal execution and
transfers control to error-handling code. You’ve learned how to throw built-in and custom exceptions, how to
include meaningful messages, how to preserve context with InnerException
, and
how to avoid common mistakes. You’ve also seen how exceptions behave in constructors, property setters, and
async methods.
In the next article, we’ll explore Exception Best Practices - how to design your error-handling strategy, when to catch vs rethrow, and how to build resilient, maintainable applications that handle failure gracefully.