C#idiomatic C#
exception-handling-statementscsharp/language-reference/statements

Language

Exception-handling statements - `throw`, `try-catch`, `try-finally`, and `try-catch-finally`

Use the C# throw statement to signal an occurrence of an exception. Use the C# try statements to catch and process exceptions occurred in a block of code.

  • page csharp/language-reference/statements/exception-handling-statements
  • updated 01/16/2026

Use the throw and try statements to work with exceptions. Use the throw statement to throw an exception. Use the try statement to catch and handle exceptions that might occur during execution of a code block.


ms.custom: "updateeachrelease"

The C# language reference documents the most recently released version of the C# language. It also contains initial documentation for features in public previews for the upcoming language release.

The documentation identifies any feature first introduced in the last three versions of the language or in current public previews.

The throw statement

The throw statement throws an exception:

sample · csharpdocs/csharp/language-reference/statements/snippets/exception-handling-statements/Program.csThrow

csharp
await ExceptionFromAsyncExample.Run();

static void ProcessShapes(int shapeAmount)
{
    // <Throw>
    if (shapeAmount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(shapeAmount), "Amount of shapes must be positive.");
    }
    // </Throw>
}

static void Rethrow()
{
    var shapeAmount = 3;
    // <Rethrow>
    try
    {
        ProcessShapes(shapeAmount);
    }
    catch (Exception e)
    {
        LogError(e, "Shape processing failed.");
        throw;
    }
    // </Rethrow>
}

static void DisplayFirstNumber(string[] args)
{
    // <ThrowExpressionConditional>
    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    // </ThrowExpressionConditional>

    var message = int.TryParse(first, out var number)
        ? $"You entered number {number}."
        : $"{first} is not an integer number.";
    Console.WriteLine(message);
}

// <ThrowExpressionExpressionBody>
DateTime ToDateTime(IFormatProvider provider) =>
         throw new InvalidCastException("Conversion to a DateTime is not supported.");
// </ThrowExpressionExpressionBody>

static void TryCatch()
{
    // <TryCatch>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // </TryCatch>
}

static async Task TryMultipleCatch()
{
    CancellationToken cancellationToken = default;

    // <TryMultipleCatch>
    try
    {
        var result = await ProcessAsync(-3, 4, cancellationToken);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Processing is cancelled.");
    }
    // </TryMultipleCatch>

    async Task<int> ProcessAsync(int a, int b, CancellationToken ct)
    {
        if (a < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(a), "The first argument cannot be negative.");
        }

        return a / (a + b);
    }
}

static void RethrowInCatch()
{
    // <RethrowInCatch>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (Exception e)
    {
        LogError(e, "Processing failed.");
        throw;
    }
    // </RethrowInCatch>
}

static void WhenFilter()
{
    // <WhenFilter>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (Exception e) when (e is ArgumentException || e is DivideByZeroException)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // </WhenFilter>
}

static int Process(int a, int b)
{
    if (a < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(a), "The first argument cannot be negative.");
    }
    return a / (a + b);
}

static void LogError(Exception e, string message) {}

In a throw e; statement, the result of expression e must be implicitly convertible to System.Exception.

You can use the built-in exception classes, such as System.ArgumentOutOfRangeException or System.InvalidOperationException. .NET also provides the following helper methods to throw exceptions in certain conditions: System.ArgumentNullException.ThrowIfNull and System.ArgumentException.ThrowIfNullOrEmpty. You can also define your own exception classes that derive from System.Exception. For more information, see Creating and throwing exceptions.

Inside a catch block, use a throw; statement to re-throw the exception that the catch block handles:

sample · csharpdocs/csharp/language-reference/statements/snippets/exception-handling-statements/Program.csRethrow

csharp
await ExceptionFromAsyncExample.Run();

static void ProcessShapes(int shapeAmount)
{
    // <Throw>
    if (shapeAmount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(shapeAmount), "Amount of shapes must be positive.");
    }
    // </Throw>
}

static void Rethrow()
{
    var shapeAmount = 3;
    // <Rethrow>
    try
    {
        ProcessShapes(shapeAmount);
    }
    catch (Exception e)
    {
        LogError(e, "Shape processing failed.");
        throw;
    }
    // </Rethrow>
}

static void DisplayFirstNumber(string[] args)
{
    // <ThrowExpressionConditional>
    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    // </ThrowExpressionConditional>

    var message = int.TryParse(first, out var number)
        ? $"You entered number {number}."
        : $"{first} is not an integer number.";
    Console.WriteLine(message);
}

// <ThrowExpressionExpressionBody>
DateTime ToDateTime(IFormatProvider provider) =>
         throw new InvalidCastException("Conversion to a DateTime is not supported.");
// </ThrowExpressionExpressionBody>

static void TryCatch()
{
    // <TryCatch>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // </TryCatch>
}

static async Task TryMultipleCatch()
{
    CancellationToken cancellationToken = default;

    // <TryMultipleCatch>
    try
    {
        var result = await ProcessAsync(-3, 4, cancellationToken);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Processing is cancelled.");
    }
    // </TryMultipleCatch>

    async Task<int> ProcessAsync(int a, int b, CancellationToken ct)
    {
        if (a < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(a), "The first argument cannot be negative.");
        }

        return a / (a + b);
    }
}

static void RethrowInCatch()
{
    // <RethrowInCatch>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (Exception e)
    {
        LogError(e, "Processing failed.");
        throw;
    }
    // </RethrowInCatch>
}

static void WhenFilter()
{
    // <WhenFilter>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (Exception e) when (e is ArgumentException || e is DivideByZeroException)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // </WhenFilter>
}

static int Process(int a, int b)
{
    if (a < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(a), "The first argument cannot be negative.");
    }
    return a / (a + b);
}

static void LogError(Exception e, string message) {}

When an exception is thrown, the common language runtime (CLR) looks for the catch block that can handle this exception. If the currently executed method doesn't contain such a catch block, the CLR looks at the method that called the current method, and so on up the call stack. If there's no compatible catch block, the CLR terminates the executing thread. For more information, see the How exceptions are handled section of the C# language specification.

The throw expression

You can also use throw as an expression. This approach might be convenient in several cases, including:

sample · csharpdocs/csharp/language-reference/statements/snippets/exception-handling-statements/Program.csThrowExpressionConditional

csharp
await ExceptionFromAsyncExample.Run();

static void ProcessShapes(int shapeAmount)
{
    // <Throw>
    if (shapeAmount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(shapeAmount), "Amount of shapes must be positive.");
    }
    // </Throw>
}

static void Rethrow()
{
    var shapeAmount = 3;
    // <Rethrow>
    try
    {
        ProcessShapes(shapeAmount);
    }
    catch (Exception e)
    {
        LogError(e, "Shape processing failed.");
        throw;
    }
    // </Rethrow>
}

static void DisplayFirstNumber(string[] args)
{
    // <ThrowExpressionConditional>
    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    // </ThrowExpressionConditional>

    var message = int.TryParse(first, out var number)
        ? $"You entered number {number}."
        : $"{first} is not an integer number.";
    Console.WriteLine(message);
}

// <ThrowExpressionExpressionBody>
DateTime ToDateTime(IFormatProvider provider) =>
         throw new InvalidCastException("Conversion to a DateTime is not supported.");
// </ThrowExpressionExpressionBody>

static void TryCatch()
{
    // <TryCatch>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // </TryCatch>
}

static async Task TryMultipleCatch()
{
    CancellationToken cancellationToken = default;

    // <TryMultipleCatch>
    try
    {
        var result = await ProcessAsync(-3, 4, cancellationToken);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Processing is cancelled.");
    }
    // </TryMultipleCatch>

    async Task<int> ProcessAsync(int a, int b, CancellationToken ct)
    {
        if (a < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(a), "The first argument cannot be negative.");
        }

        return a / (a + b);
    }
}

static void RethrowInCatch()
{
    // <RethrowInCatch>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (Exception e)
    {
        LogError(e, "Processing failed.");
        throw;
    }
    // </RethrowInCatch>
}

static void WhenFilter()
{
    // <WhenFilter>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (Exception e) when (e is ArgumentException || e is DivideByZeroException)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // </WhenFilter>
}

static int Process(int a, int b)
{
    if (a < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(a), "The first argument cannot be negative.");
    }
    return a / (a + b);
}

static void LogError(Exception e, string message) {}

sample · csharpdocs/csharp/language-reference/statements/snippets/exception-handling-statements/ExampleClass.csThrowExpressionCoalescing

csharp
public class ExampleClass
{
    string name;

    // <ThrowExpressionCoalescing>
    public string Name
    {
        get => name;
        set => name = value ??
            throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
    }
    // </ThrowExpressionCoalescing>

    public bool Busy { get; private set; }

    // <TryFinally>
    public async Task HandleRequest(int itemId, CancellationToken ct)
    {
        Busy = true;

        try
        {
            await ProcessAsync(itemId, ct);
        }
        finally
        {
            Busy = false;
        }
    }
    // </TryFinally>

    private async Task ProcessAsync(int itemId, CancellationToken ct)
    {
        await Task.Delay(100);
    }

    // <TryCatchFinally>
    public async Task ProcessRequest(int itemId, CancellationToken ct)
    {
        Busy = true;

        try
        {
            await ProcessAsync(itemId, ct);
        }
        catch (Exception e) when (e is not OperationCanceledException)
        {
            LogError(e, $"Failed to process request for item ID {itemId}.");
            throw;
        }
        finally
        {
            Busy = false;
        }

    }
    // </TryCatchFinally>

    private static void LogError(Exception e, string message) {}
}

sample · csharpdocs/csharp/language-reference/statements/snippets/exception-handling-statements/Program.csThrowExpressionExpressionBody

csharp
await ExceptionFromAsyncExample.Run();

static void ProcessShapes(int shapeAmount)
{
    // <Throw>
    if (shapeAmount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(shapeAmount), "Amount of shapes must be positive.");
    }
    // </Throw>
}

static void Rethrow()
{
    var shapeAmount = 3;
    // <Rethrow>
    try
    {
        ProcessShapes(shapeAmount);
    }
    catch (Exception e)
    {
        LogError(e, "Shape processing failed.");
        throw;
    }
    // </Rethrow>
}

static void DisplayFirstNumber(string[] args)
{
    // <ThrowExpressionConditional>
    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    // </ThrowExpressionConditional>

    var message = int.TryParse(first, out var number)
        ? $"You entered number {number}."
        : $"{first} is not an integer number.";
    Console.WriteLine(message);
}

// <ThrowExpressionExpressionBody>
DateTime ToDateTime(IFormatProvider provider) =>
         throw new InvalidCastException("Conversion to a DateTime is not supported.");
// </ThrowExpressionExpressionBody>

static void TryCatch()
{
    // <TryCatch>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // </TryCatch>
}

static async Task TryMultipleCatch()
{
    CancellationToken cancellationToken = default;

    // <TryMultipleCatch>
    try
    {
        var result = await ProcessAsync(-3, 4, cancellationToken);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Processing is cancelled.");
    }
    // </TryMultipleCatch>

    async Task<int> ProcessAsync(int a, int b, CancellationToken ct)
    {
        if (a < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(a), "The first argument cannot be negative.");
        }

        return a / (a + b);
    }
}

static void RethrowInCatch()
{
    // <RethrowInCatch>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (Exception e)
    {
        LogError(e, "Processing failed.");
        throw;
    }
    // </RethrowInCatch>
}

static void WhenFilter()
{
    // <WhenFilter>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (Exception e) when (e is ArgumentException || e is DivideByZeroException)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // </WhenFilter>
}

static int Process(int a, int b)
{
    if (a < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(a), "The first argument cannot be negative.");
    }
    return a / (a + b);
}

static void LogError(Exception e, string message) {}

The try statement

You can use the try statement in any of the following forms: try-catch - to handle exceptions that might occur during execution of the code inside a try block, try-finally - to specify the code that runs when control leaves the try block, and try-catch-finally - as a combination of the preceding two forms.

The try-catch statement

Use the try-catch statement to handle exceptions that might occur during execution of a code block. Place the code where an exception might occur inside a try block. Use a catch clause to specify the base type of exceptions you want to handle in the corresponding catch block:

sample · csharpdocs/csharp/language-reference/statements/snippets/exception-handling-statements/Program.csTryCatch

csharp
await ExceptionFromAsyncExample.Run();

static void ProcessShapes(int shapeAmount)
{
    // <Throw>
    if (shapeAmount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(shapeAmount), "Amount of shapes must be positive.");
    }
    // </Throw>
}

static void Rethrow()
{
    var shapeAmount = 3;
    // <Rethrow>
    try
    {
        ProcessShapes(shapeAmount);
    }
    catch (Exception e)
    {
        LogError(e, "Shape processing failed.");
        throw;
    }
    // </Rethrow>
}

static void DisplayFirstNumber(string[] args)
{
    // <ThrowExpressionConditional>
    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    // </ThrowExpressionConditional>

    var message = int.TryParse(first, out var number)
        ? $"You entered number {number}."
        : $"{first} is not an integer number.";
    Console.WriteLine(message);
}

// <ThrowExpressionExpressionBody>
DateTime ToDateTime(IFormatProvider provider) =>
         throw new InvalidCastException("Conversion to a DateTime is not supported.");
// </ThrowExpressionExpressionBody>

static void TryCatch()
{
    // <TryCatch>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // </TryCatch>
}

static async Task TryMultipleCatch()
{
    CancellationToken cancellationToken = default;

    // <TryMultipleCatch>
    try
    {
        var result = await ProcessAsync(-3, 4, cancellationToken);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Processing is cancelled.");
    }
    // </TryMultipleCatch>

    async Task<int> ProcessAsync(int a, int b, CancellationToken ct)
    {
        if (a < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(a), "The first argument cannot be negative.");
        }

        return a / (a + b);
    }
}

static void RethrowInCatch()
{
    // <RethrowInCatch>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (Exception e)
    {
        LogError(e, "Processing failed.");
        throw;
    }
    // </RethrowInCatch>
}

static void WhenFilter()
{
    // <WhenFilter>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (Exception e) when (e is ArgumentException || e is DivideByZeroException)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // </WhenFilter>
}

static int Process(int a, int b)
{
    if (a < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(a), "The first argument cannot be negative.");
    }
    return a / (a + b);
}

static void LogError(Exception e, string message) {}

You can provide several catch clauses:

sample · csharpdocs/csharp/language-reference/statements/snippets/exception-handling-statements/Program.csTryMultipleCatch

csharp
await ExceptionFromAsyncExample.Run();

static void ProcessShapes(int shapeAmount)
{
    // <Throw>
    if (shapeAmount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(shapeAmount), "Amount of shapes must be positive.");
    }
    // </Throw>
}

static void Rethrow()
{
    var shapeAmount = 3;
    // <Rethrow>
    try
    {
        ProcessShapes(shapeAmount);
    }
    catch (Exception e)
    {
        LogError(e, "Shape processing failed.");
        throw;
    }
    // </Rethrow>
}

static void DisplayFirstNumber(string[] args)
{
    // <ThrowExpressionConditional>
    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    // </ThrowExpressionConditional>

    var message = int.TryParse(first, out var number)
        ? $"You entered number {number}."
        : $"{first} is not an integer number.";
    Console.WriteLine(message);
}

// <ThrowExpressionExpressionBody>
DateTime ToDateTime(IFormatProvider provider) =>
         throw new InvalidCastException("Conversion to a DateTime is not supported.");
// </ThrowExpressionExpressionBody>

static void TryCatch()
{
    // <TryCatch>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // </TryCatch>
}

static async Task TryMultipleCatch()
{
    CancellationToken cancellationToken = default;

    // <TryMultipleCatch>
    try
    {
        var result = await ProcessAsync(-3, 4, cancellationToken);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Processing is cancelled.");
    }
    // </TryMultipleCatch>

    async Task<int> ProcessAsync(int a, int b, CancellationToken ct)
    {
        if (a < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(a), "The first argument cannot be negative.");
        }

        return a / (a + b);
    }
}

static void RethrowInCatch()
{
    // <RethrowInCatch>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (Exception e)
    {
        LogError(e, "Processing failed.");
        throw;
    }
    // </RethrowInCatch>
}

static void WhenFilter()
{
    // <WhenFilter>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (Exception e) when (e is ArgumentException || e is DivideByZeroException)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // </WhenFilter>
}

static int Process(int a, int b)
{
    if (a < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(a), "The first argument cannot be negative.");
    }
    return a / (a + b);
}

static void LogError(Exception e, string message) {}

When an exception occurs, the runtime checks catch clauses in the specified order, from top to bottom. At most, only one catch block runs for any thrown exception. As the preceding example also shows, you can omit declaration of an exception variable and specify only the exception type in a catch clause. A catch clause without any specified exception type matches any exception and, if present, must be the last catch clause.

To re-throw a caught exception, use the throw statement, as the following example shows:

sample · csharpdocs/csharp/language-reference/statements/snippets/exception-handling-statements/Program.csRethrowInCatch

csharp
await ExceptionFromAsyncExample.Run();

static void ProcessShapes(int shapeAmount)
{
    // <Throw>
    if (shapeAmount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(shapeAmount), "Amount of shapes must be positive.");
    }
    // </Throw>
}

static void Rethrow()
{
    var shapeAmount = 3;
    // <Rethrow>
    try
    {
        ProcessShapes(shapeAmount);
    }
    catch (Exception e)
    {
        LogError(e, "Shape processing failed.");
        throw;
    }
    // </Rethrow>
}

static void DisplayFirstNumber(string[] args)
{
    // <ThrowExpressionConditional>
    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    // </ThrowExpressionConditional>

    var message = int.TryParse(first, out var number)
        ? $"You entered number {number}."
        : $"{first} is not an integer number.";
    Console.WriteLine(message);
}

// <ThrowExpressionExpressionBody>
DateTime ToDateTime(IFormatProvider provider) =>
         throw new InvalidCastException("Conversion to a DateTime is not supported.");
// </ThrowExpressionExpressionBody>

static void TryCatch()
{
    // <TryCatch>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // </TryCatch>
}

static async Task TryMultipleCatch()
{
    CancellationToken cancellationToken = default;

    // <TryMultipleCatch>
    try
    {
        var result = await ProcessAsync(-3, 4, cancellationToken);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Processing is cancelled.");
    }
    // </TryMultipleCatch>

    async Task<int> ProcessAsync(int a, int b, CancellationToken ct)
    {
        if (a < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(a), "The first argument cannot be negative.");
        }

        return a / (a + b);
    }
}

static void RethrowInCatch()
{
    // <RethrowInCatch>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (Exception e)
    {
        LogError(e, "Processing failed.");
        throw;
    }
    // </RethrowInCatch>
}

static void WhenFilter()
{
    // <WhenFilter>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (Exception e) when (e is ArgumentException || e is DivideByZeroException)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // </WhenFilter>
}

static int Process(int a, int b)
{
    if (a < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(a), "The first argument cannot be negative.");
    }
    return a / (a + b);
}

static void LogError(Exception e, string message) {}

A when exception filter

Along with an exception type, you can also specify an exception filter that further examines an exception and decides if the corresponding catch block handles that exception. An exception filter is a Boolean expression that follows the when keyword, as the following example shows:

sample · csharpdocs/csharp/language-reference/statements/snippets/exception-handling-statements/Program.csWhenFilter

csharp
await ExceptionFromAsyncExample.Run();

static void ProcessShapes(int shapeAmount)
{
    // <Throw>
    if (shapeAmount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(shapeAmount), "Amount of shapes must be positive.");
    }
    // </Throw>
}

static void Rethrow()
{
    var shapeAmount = 3;
    // <Rethrow>
    try
    {
        ProcessShapes(shapeAmount);
    }
    catch (Exception e)
    {
        LogError(e, "Shape processing failed.");
        throw;
    }
    // </Rethrow>
}

static void DisplayFirstNumber(string[] args)
{
    // <ThrowExpressionConditional>
    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    // </ThrowExpressionConditional>

    var message = int.TryParse(first, out var number)
        ? $"You entered number {number}."
        : $"{first} is not an integer number.";
    Console.WriteLine(message);
}

// <ThrowExpressionExpressionBody>
DateTime ToDateTime(IFormatProvider provider) =>
         throw new InvalidCastException("Conversion to a DateTime is not supported.");
// </ThrowExpressionExpressionBody>

static void TryCatch()
{
    // <TryCatch>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // </TryCatch>
}

static async Task TryMultipleCatch()
{
    CancellationToken cancellationToken = default;

    // <TryMultipleCatch>
    try
    {
        var result = await ProcessAsync(-3, 4, cancellationToken);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Processing is cancelled.");
    }
    // </TryMultipleCatch>

    async Task<int> ProcessAsync(int a, int b, CancellationToken ct)
    {
        if (a < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(a), "The first argument cannot be negative.");
        }

        return a / (a + b);
    }
}

static void RethrowInCatch()
{
    // <RethrowInCatch>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (Exception e)
    {
        LogError(e, "Processing failed.");
        throw;
    }
    // </RethrowInCatch>
}

static void WhenFilter()
{
    // <WhenFilter>
    try
    {
        var result = Process(-3, 4);
        Console.WriteLine($"Processing succeeded: {result}");
    }
    catch (Exception e) when (e is ArgumentException || e is DivideByZeroException)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // </WhenFilter>
}

static int Process(int a, int b)
{
    if (a < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(a), "The first argument cannot be negative.");
    }
    return a / (a + b);
}

static void LogError(Exception e, string message) {}

The preceding example uses an exception filter to provide a single catch block to handle exceptions of two specified types.

You can provide several catch clauses for the same exception type if they distinguish by exception filters. One of those clauses might have no exception filter. If such a clause exists, it must be the last of the clauses that specify that exception type.

If a catch clause has an exception filter, it can specify the exception type that is the same as or less derived than an exception type of a catch clause that appears after it. For example, if an exception filter is present, a catch (Exception e) clause doesn't need to be the last clause.

Exception filters vs. traditional exception handling

Exception filters provide significant advantages over traditional exception handling approaches. The key difference is when the exception handling logic is evaluated:

  • Exception filters (when): The filter expression is evaluated before the stack is unwound. This means the original call stack and all local variables remain intact during filter evaluation.
  • Traditional catch blocks: The catch block runs after the stack is unwound, potentially losing valuable debugging information.

Here's a comparison showing the difference:

sample · csharpdocs/csharp/language-reference/statements/snippets/exception-handling-statements/WhenFilterExamples.csExceptionFilterVsIfElse

csharp
using System;
using System.Collections.Generic;
using System.IO;

namespace ExceptionFilterExamples
{
    public static class WhenFilterExamples
    {
        // <ExceptionFilterVsIfElse>
        public static void DemonstrateStackUnwindingDifference()
        {
            var localVariable = "Important debugging info";
            
            try
            {
                ProcessWithExceptionFilter(localVariable);
            }
            catch (InvalidOperationException ex) when (ex.Message.Contains("filter"))
            {
                // Exception filter: Stack not unwound yet.
                // localVariable is still accessible in debugger.
                // Call stack shows original throwing location.
                Console.WriteLine($"Caught with filter: {ex.Message}");
                Console.WriteLine($"Local variable accessible: {localVariable}");
            }
            
            try
            {
                ProcessWithTraditionalCatch(localVariable);
            }
            catch (InvalidOperationException ex)
            {
                // Traditional catch: Stack already unwound.
                // Some debugging information may be lost.
                if (ex.Message.Contains("traditional"))
                {
                    Console.WriteLine($"Caught with if: {ex.Message}");
                    Console.WriteLine($"Local variable accessible: {localVariable}");
                }
                else
                {
                    throw; // Re-throws and further modifies stack trace.
                }
            }
        }

        private static void ProcessWithExceptionFilter(string context)
        {
            throw new InvalidOperationException($"Exception for filter demo: {context}");
        }

        private static void ProcessWithTraditionalCatch(string context)
        {
            throw new InvalidOperationException($"Exception for traditional demo: {context}");
        }
        // </ExceptionFilterVsIfElse>

        // <MultipleConditionsExample>
        public static void HandleFileOperations(string filePath)
        {
            try
            {
                // Simulate file operation that might fail.
                ProcessFile(filePath);
            }
            catch (IOException ex) when (ex.Message.Contains("access denied"))
            {
                Console.WriteLine("File access denied. Check permissions.");
            }
            catch (IOException ex) when (ex.Message.Contains("not found"))
            {
                Console.WriteLine("File not found. Verify the path.");
            }
            catch (IOException ex) when (IsNetworkPath(filePath))
            {
                Console.WriteLine($"Network file operation failed: {ex.Message}");
            }
            catch (IOException)
            {
                Console.WriteLine("Other I/O error occurred.");
            }
        }

        private static void ProcessFile(string filePath)
        {
            // Simulate different types of file exceptions.
            if (filePath.Contains("denied"))
                throw new IOException("File access denied");
            if (filePath.Contains("missing"))
                throw new IOException("File not found");
            if (IsNetworkPath(filePath))
                throw new IOException("Network timeout occurred");
        }

        private static bool IsNetworkPath(string path)
        {
            return path.StartsWith(@"\\") || path.StartsWith("http");
        }
        // </MultipleConditionsExample>

        // <DebuggingAdvantageExample>
        public static void DemonstrateDebuggingAdvantage()
        {
            var contextData = new Dictionary<string, object>
            {
                ["RequestId"] = Guid.NewGuid(),
                ["UserId"] = "user123",
                ["Timestamp"] = DateTime.Now
            };

            try
            {
                // Simulate a deep call stack.
                Level1Method(contextData);
            }
            catch (Exception ex) when (LogAndFilter(ex, contextData))
            {
                // This catch block may never execute if LogAndFilter returns false.
                // But LogAndFilter can examine the exception while the stack is intact.
                Console.WriteLine("Exception handled after logging");
            }
        }

        private static void Level1Method(Dictionary<string, object> context)
        {
            Level2Method(context);
        }

        private static void Level2Method(Dictionary<string, object> context)
        {
            Level3Method(context);
        }

        private static void Level3Method(Dictionary<string, object> context)
        {
            throw new InvalidOperationException("Error in deep call stack");
        }

        private static bool LogAndFilter(Exception ex, Dictionary<string, object> context)
        {
            // This method runs before stack unwinding.
            // Full call stack and local variables are still available.
            Console.WriteLine($"Exception occurred: {ex.Message}");
            Console.WriteLine($"Request ID: {context["RequestId"]}");
            Console.WriteLine($"Full stack trace preserved: {ex.StackTrace}");
            
            // Return true to handle the exception, false to continue search.
            return ex.Message.Contains("deep call stack");
        }
        // </DebuggingAdvantageExample>
    }
}

Advantages of exception filters

  • Better debugging experience: Since the stack isn't unwound until a filter matches, debuggers can show the original point of failure with all local variables intact.
  • Performance benefits: If no filter matches, the exception continues propagating without the overhead of stack unwinding and restoration.
  • Cleaner code: Multiple filters can handle different conditions of the same exception type without requiring nested if-else statements.
  • Logging and diagnostics: You can examine and log exception details before deciding whether to handle the exception:

sample · csharpdocs/csharp/language-reference/statements/snippets/exception-handling-statements/WhenFilterExamples.csDebuggingAdvantageExample

csharp
using System;
using System.Collections.Generic;
using System.IO;

namespace ExceptionFilterExamples
{
    public static class WhenFilterExamples
    {
        // <ExceptionFilterVsIfElse>
        public static void DemonstrateStackUnwindingDifference()
        {
            var localVariable = "Important debugging info";
            
            try
            {
                ProcessWithExceptionFilter(localVariable);
            }
            catch (InvalidOperationException ex) when (ex.Message.Contains("filter"))
            {
                // Exception filter: Stack not unwound yet.
                // localVariable is still accessible in debugger.
                // Call stack shows original throwing location.
                Console.WriteLine($"Caught with filter: {ex.Message}");
                Console.WriteLine($"Local variable accessible: {localVariable}");
            }
            
            try
            {
                ProcessWithTraditionalCatch(localVariable);
            }
            catch (InvalidOperationException ex)
            {
                // Traditional catch: Stack already unwound.
                // Some debugging information may be lost.
                if (ex.Message.Contains("traditional"))
                {
                    Console.WriteLine($"Caught with if: {ex.Message}");
                    Console.WriteLine($"Local variable accessible: {localVariable}");
                }
                else
                {
                    throw; // Re-throws and further modifies stack trace.
                }
            }
        }

        private static void ProcessWithExceptionFilter(string context)
        {
            throw new InvalidOperationException($"Exception for filter demo: {context}");
        }

        private static void ProcessWithTraditionalCatch(string context)
        {
            throw new InvalidOperationException($"Exception for traditional demo: {context}");
        }
        // </ExceptionFilterVsIfElse>

        // <MultipleConditionsExample>
        public static void HandleFileOperations(string filePath)
        {
            try
            {
                // Simulate file operation that might fail.
                ProcessFile(filePath);
            }
            catch (IOException ex) when (ex.Message.Contains("access denied"))
            {
                Console.WriteLine("File access denied. Check permissions.");
            }
            catch (IOException ex) when (ex.Message.Contains("not found"))
            {
                Console.WriteLine("File not found. Verify the path.");
            }
            catch (IOException ex) when (IsNetworkPath(filePath))
            {
                Console.WriteLine($"Network file operation failed: {ex.Message}");
            }
            catch (IOException)
            {
                Console.WriteLine("Other I/O error occurred.");
            }
        }

        private static void ProcessFile(string filePath)
        {
            // Simulate different types of file exceptions.
            if (filePath.Contains("denied"))
                throw new IOException("File access denied");
            if (filePath.Contains("missing"))
                throw new IOException("File not found");
            if (IsNetworkPath(filePath))
                throw new IOException("Network timeout occurred");
        }

        private static bool IsNetworkPath(string path)
        {
            return path.StartsWith(@"\\") || path.StartsWith("http");
        }
        // </MultipleConditionsExample>

        // <DebuggingAdvantageExample>
        public static void DemonstrateDebuggingAdvantage()
        {
            var contextData = new Dictionary<string, object>
            {
                ["RequestId"] = Guid.NewGuid(),
                ["UserId"] = "user123",
                ["Timestamp"] = DateTime.Now
            };

            try
            {
                // Simulate a deep call stack.
                Level1Method(contextData);
            }
            catch (Exception ex) when (LogAndFilter(ex, contextData))
            {
                // This catch block may never execute if LogAndFilter returns false.
                // But LogAndFilter can examine the exception while the stack is intact.
                Console.WriteLine("Exception handled after logging");
            }
        }

        private static void Level1Method(Dictionary<string, object> context)
        {
            Level2Method(context);
        }

        private static void Level2Method(Dictionary<string, object> context)
        {
            Level3Method(context);
        }

        private static void Level3Method(Dictionary<string, object> context)
        {
            throw new InvalidOperationException("Error in deep call stack");
        }

        private static bool LogAndFilter(Exception ex, Dictionary<string, object> context)
        {
            // This method runs before stack unwinding.
            // Full call stack and local variables are still available.
            Console.WriteLine($"Exception occurred: {ex.Message}");
            Console.WriteLine($"Request ID: {context["RequestId"]}");
            Console.WriteLine($"Full stack trace preserved: {ex.StackTrace}");
            
            // Return true to handle the exception, false to continue search.
            return ex.Message.Contains("deep call stack");
        }
        // </DebuggingAdvantageExample>
    }
}

When to use exception filters

Use exception filters when you need to:

  • Handle exceptions based on specific conditions or properties.
  • Preserve the original call stack for debugging.
  • Log or examine exceptions before deciding whether to handle them.
  • Handle the same exception type differently based on context.

sample · csharpdocs/csharp/language-reference/statements/snippets/exception-handling-statements/WhenFilterExamples.csMultipleConditionsExample

csharp
using System;
using System.Collections.Generic;
using System.IO;

namespace ExceptionFilterExamples
{
    public static class WhenFilterExamples
    {
        // <ExceptionFilterVsIfElse>
        public static void DemonstrateStackUnwindingDifference()
        {
            var localVariable = "Important debugging info";
            
            try
            {
                ProcessWithExceptionFilter(localVariable);
            }
            catch (InvalidOperationException ex) when (ex.Message.Contains("filter"))
            {
                // Exception filter: Stack not unwound yet.
                // localVariable is still accessible in debugger.
                // Call stack shows original throwing location.
                Console.WriteLine($"Caught with filter: {ex.Message}");
                Console.WriteLine($"Local variable accessible: {localVariable}");
            }
            
            try
            {
                ProcessWithTraditionalCatch(localVariable);
            }
            catch (InvalidOperationException ex)
            {
                // Traditional catch: Stack already unwound.
                // Some debugging information may be lost.
                if (ex.Message.Contains("traditional"))
                {
                    Console.WriteLine($"Caught with if: {ex.Message}");
                    Console.WriteLine($"Local variable accessible: {localVariable}");
                }
                else
                {
                    throw; // Re-throws and further modifies stack trace.
                }
            }
        }

        private static void ProcessWithExceptionFilter(string context)
        {
            throw new InvalidOperationException($"Exception for filter demo: {context}");
        }

        private static void ProcessWithTraditionalCatch(string context)
        {
            throw new InvalidOperationException($"Exception for traditional demo: {context}");
        }
        // </ExceptionFilterVsIfElse>

        // <MultipleConditionsExample>
        public static void HandleFileOperations(string filePath)
        {
            try
            {
                // Simulate file operation that might fail.
                ProcessFile(filePath);
            }
            catch (IOException ex) when (ex.Message.Contains("access denied"))
            {
                Console.WriteLine("File access denied. Check permissions.");
            }
            catch (IOException ex) when (ex.Message.Contains("not found"))
            {
                Console.WriteLine("File not found. Verify the path.");
            }
            catch (IOException ex) when (IsNetworkPath(filePath))
            {
                Console.WriteLine($"Network file operation failed: {ex.Message}");
            }
            catch (IOException)
            {
                Console.WriteLine("Other I/O error occurred.");
            }
        }

        private static void ProcessFile(string filePath)
        {
            // Simulate different types of file exceptions.
            if (filePath.Contains("denied"))
                throw new IOException("File access denied");
            if (filePath.Contains("missing"))
                throw new IOException("File not found");
            if (IsNetworkPath(filePath))
                throw new IOException("Network timeout occurred");
        }

        private static bool IsNetworkPath(string path)
        {
            return path.StartsWith(@"\\") || path.StartsWith("http");
        }
        // </MultipleConditionsExample>

        // <DebuggingAdvantageExample>
        public static void DemonstrateDebuggingAdvantage()
        {
            var contextData = new Dictionary<string, object>
            {
                ["RequestId"] = Guid.NewGuid(),
                ["UserId"] = "user123",
                ["Timestamp"] = DateTime.Now
            };

            try
            {
                // Simulate a deep call stack.
                Level1Method(contextData);
            }
            catch (Exception ex) when (LogAndFilter(ex, contextData))
            {
                // This catch block may never execute if LogAndFilter returns false.
                // But LogAndFilter can examine the exception while the stack is intact.
                Console.WriteLine("Exception handled after logging");
            }
        }

        private static void Level1Method(Dictionary<string, object> context)
        {
            Level2Method(context);
        }

        private static void Level2Method(Dictionary<string, object> context)
        {
            Level3Method(context);
        }

        private static void Level3Method(Dictionary<string, object> context)
        {
            throw new InvalidOperationException("Error in deep call stack");
        }

        private static bool LogAndFilter(Exception ex, Dictionary<string, object> context)
        {
            // This method runs before stack unwinding.
            // Full call stack and local variables are still available.
            Console.WriteLine($"Exception occurred: {ex.Message}");
            Console.WriteLine($"Request ID: {context["RequestId"]}");
            Console.WriteLine($"Full stack trace preserved: {ex.StackTrace}");
            
            // Return true to handle the exception, false to continue search.
            return ex.Message.Contains("deep call stack");
        }
        // </DebuggingAdvantageExample>
    }
}

Stack trace preservation

Exception filters preserve the original ex.StackTrace property. If a catch clause can't process the exception and re-throws it, the original stack information is lost. The when filter doesn't unwind the stack, so if a when filter is false, the original stack trace isn't changed.

The exception filter approach is valuable in applications where preserving debugging information is crucial for diagnosing code errors.

Exceptions in async and iterator methods

If an exception occurs in an async function, the exception propagates to the caller of the function when you await the result of the function, as the following example shows:

sample · csharpdocs/csharp/language-reference/statements/snippets/exception-handling-statements/ExceptionFromAsyncExample.csExceptionFromAsync

csharp
public class ExceptionFromAsyncExample
{
    // <ExceptionFromAsync>
    public static async Task Run()
    {
        try
        {
            Task<int> processing = ProcessAsync(-1);
            Console.WriteLine("Launched processing.");

            int result = await processing;
            Console.WriteLine($"Result: {result}.");
        }
        catch (ArgumentException e)
        {
            Console.WriteLine($"Processing failed: {e.Message}");
        }
        // Output:
        // Launched processing.
        // Processing failed: Input must be non-negative. (Parameter 'input')
    }

    private static async Task<int> ProcessAsync(int input)
    {
        if (input < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(input), "Input must be non-negative.");
        }

        await Task.Delay(500);
        return input;
    }
    // </ExceptionFromAsync>
}

If an exception occurs in an iterator method, the exception propagates to the caller only when the iterator advances to the next element.

The try-finally statement

In a try-finally statement, the finally block runs when control leaves the try block. Control might leave the try block as a result of

  • normal execution,
  • execution of a jump statement (that is, return, break, continue, or goto), or
  • propagation of an exception out of the try block.

The following example uses the finally block to reset the state of an object before control leaves the method:

sample · csharpdocs/csharp/language-reference/statements/snippets/exception-handling-statements/ExampleClass.csTryFinally

csharp
public class ExampleClass
{
    string name;

    // <ThrowExpressionCoalescing>
    public string Name
    {
        get => name;
        set => name = value ??
            throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
    }
    // </ThrowExpressionCoalescing>

    public bool Busy { get; private set; }

    // <TryFinally>
    public async Task HandleRequest(int itemId, CancellationToken ct)
    {
        Busy = true;

        try
        {
            await ProcessAsync(itemId, ct);
        }
        finally
        {
            Busy = false;
        }
    }
    // </TryFinally>

    private async Task ProcessAsync(int itemId, CancellationToken ct)
    {
        await Task.Delay(100);
    }

    // <TryCatchFinally>
    public async Task ProcessRequest(int itemId, CancellationToken ct)
    {
        Busy = true;

        try
        {
            await ProcessAsync(itemId, ct);
        }
        catch (Exception e) when (e is not OperationCanceledException)
        {
            LogError(e, $"Failed to process request for item ID {itemId}.");
            throw;
        }
        finally
        {
            Busy = false;
        }

    }
    // </TryCatchFinally>

    private static void LogError(Exception e, string message) {}
}

You can also use the finally block to clean up allocated resources used in the try block.

Whether the finally block executes depends on whether the operating system chooses to trigger an exception unwind operation. The only cases where finally blocks don't execute involve immediate termination of a program. For example, such a termination might happen because of the System.Environment.FailFast call or an System.OverflowException or System.InvalidProgramException exception. Most operating systems perform reasonable resource clean-up as part of stopping and unloading the process.

The try-catch-finally statement

Use a try-catch-finally statement both to handle exceptions that might occur during execution of the try block and to specify the code that must run when control leaves the try statement:

sample · csharpdocs/csharp/language-reference/statements/snippets/exception-handling-statements/ExampleClass.csTryCatchFinally

csharp
public class ExampleClass
{
    string name;

    // <ThrowExpressionCoalescing>
    public string Name
    {
        get => name;
        set => name = value ??
            throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
    }
    // </ThrowExpressionCoalescing>

    public bool Busy { get; private set; }

    // <TryFinally>
    public async Task HandleRequest(int itemId, CancellationToken ct)
    {
        Busy = true;

        try
        {
            await ProcessAsync(itemId, ct);
        }
        finally
        {
            Busy = false;
        }
    }
    // </TryFinally>

    private async Task ProcessAsync(int itemId, CancellationToken ct)
    {
        await Task.Delay(100);
    }

    // <TryCatchFinally>
    public async Task ProcessRequest(int itemId, CancellationToken ct)
    {
        Busy = true;

        try
        {
            await ProcessAsync(itemId, ct);
        }
        catch (Exception e) when (e is not OperationCanceledException)
        {
            LogError(e, $"Failed to process request for item ID {itemId}.");
            throw;
        }
        finally
        {
            Busy = false;
        }

    }
    // </TryCatchFinally>

    private static void LogError(Exception e, string message) {}
}

When a catch block handles an exception, the finally block runs after the catch block finishes (even if another exception occurs during execution of the catch block). For information about catch and finally blocks, see The try-catch statement and The try-finally statement sections, respectively.

C# language specification

For more information, see the following sections of the C# language specification:

See also