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.