throw / Custom Exceptions
| Since: | C# 1.0(2002) |
|---|
The throw keyword intentionally raises an exception, and a custom exception class lets you carry application-specific error information.
Syntax
throw new ExceptionType("message");
class ClassName : Exception
{
public ClassName(string message) : base(message) { }
}
Syntax / Member List
| Syntax / Member | Description |
|---|---|
| throw new Exception("msg") | Throws an exception. Interrupts the current method call and transfers control to the nearest catch block. |
| throw; | Used inside a catch block to rethrow the caught exception while preserving the original stack trace. |
| Exception | The base class for all exceptions. |
| ArgumentException | Thrown when an argument is invalid. |
| ArgumentNullException | Thrown when an argument is null. |
| InvalidOperationException | Thrown when an operation is invalid for the current state of an object. |
| NotImplementedException | Used to indicate that a method has not been implemented yet. |
Sample Code
Program.cs
using System;
// Throw an exception with throw.
static void CheckAge(int age)
{
if (age < 0 || age > 150)
throw new ArgumentOutOfRangeException(nameof(age), "Age must be between 0 and 150.");
Console.WriteLine($"Age: {age}");
}
try
{
CheckAge(200);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine(e.Message);
}
class AppException : Exception
{
public int StatusCode { get; }
public AppException(string message, int statusCode) : base(message)
{
StatusCode = statusCode;
}
}
// Throw and catch a custom exception.
try
{
throw new AppException("Authentication failed.", 401);
}
catch (AppException e)
{
Console.WriteLine($"[{e.StatusCode}] {e.Message}");
}
// Rethrow inside a catch block (preserves the stack trace).
static void Process()
{
try
{
int result = int.Parse("abc");
}
catch (FormatException)
{
Console.WriteLine("Log: Conversion error detected.");
throw; // Rethrow the exception (stack trace is preserved).
}
}
try
{
Process();
}
catch (FormatException e)
{
Console.WriteLine("Caught at upper level: " + e.Message);
}
This produces the following output:
dotnet script throw_custom_exception.csx Age must be between 0 and 150. (Parameter 'age') [401] Authentication failed. Log: Conversion error detected. Caught at upper level: The input string 'abc' was not in a correct format.
Practical Pattern: Custom Exception with Detailed Information
A custom exception class implementation that carries application-specific error codes and additional information as properties.
CustomExceptions.cs
using System;
using System.Collections.Generic;
// A custom exception representing an API error.
class ApiException : Exception
{
public int StatusCode { get; }
public string ErrorCode { get; }
public object? Detail { get; }
public ApiException(string message, int statusCode, string errorCode, object? detail = null)
: base(message)
{
StatusCode = statusCode;
ErrorCode = errorCode;
Detail = detail;
}
}
// A custom exception representing a validation error.
class ValidationException : Exception
{
public IReadOnlyList<string> Errors { get; }
public ValidationException(IReadOnlyList<string> errors)
: base($"Validation error: {errors.Count} issue(s)")
{
Errors = errors;
}
}
// Simulates an API call.
static string FetchUser(int id)
{
if (id <= 0)
throw new ApiException("Invalid ID.", 400, "INVALID_ID", new { Id = id });
if (id > 100)
throw new ApiException("User not found.", 404, "USER_NOT_FOUND");
return id switch
{
1 => "user_01",
2 => "user_02",
_ => $"User-{id}"
};
}
// Catching ApiException
try { Console.WriteLine(FetchUser(-1)); }
catch (ApiException e)
{
Console.WriteLine($"API error [{e.StatusCode}] {e.ErrorCode}: {e.Message}");
if (e.Detail != null) Console.WriteLine($" Detail: {e.Detail}");
}
// Catching ValidationException
var errors = new List<string> { "Name is required.", "Age must be between 0 and 150." };
try { throw new ValidationException(errors); }
catch (ValidationException e)
{
Console.WriteLine(e.Message);
foreach (string err in e.Errors)
Console.WriteLine($" - {err}");
}
This produces the following output:
dotnet script custom_exceptions.csx
API error [400] INVALID_ID: Invalid ID.
Detail: { Id = -1 }
Validation error: 2 issue(s)
- Name is required.
- Age must be between 0 and 150.
Practical Pattern: throw Expressions (C# 7 and Later)
From C# 7 onwards, throw can be used as an expression. Combining it with the ternary operator or the null-coalescing operator makes the code more concise.
ThrowExpression.cs
using System;
// throw expression: combined with the ternary operator.
static int Divide(int a, int b)
=> b == 0
? throw new DivideByZeroException("Cannot divide by zero.")
: a / b;
// throw expression: combined with the null-coalescing operator.
static string GetName(string? name)
=> name ?? throw new ArgumentNullException(nameof(name), "Name is required.");
// throw expression: validation in a property setter.
class MissionRecord
{
private string _title = string.Empty;
public string Title
{
get => _title;
set => _title = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Title cannot be empty.", nameof(value))
: value;
}
}
// Tests
Console.WriteLine(Divide(10, 2)); // 5
try { Divide(10, 0); } catch (DivideByZeroException e) { Console.WriteLine(e.Message); }
Console.WriteLine(GetName("sample")); // sample
try { GetName(null); } catch (ArgumentNullException e) { Console.WriteLine(e.Message); }
var mission = new MissionRecord();
try
{
mission.Title = ""; // The setter throws an exception.
}
catch (ArgumentException e)
{
Console.WriteLine(e.Message);
}
This produces the following output:
dotnet script throw_expression.csx 5 Cannot divide by zero. sample Name is required. (Parameter 'name') Title cannot be empty. (Parameter 'value')
Common Mistakes
Common Mistake 1: throw e Overwrites the Stack Trace
When rethrowing an exception inside a catch block, writing throw e; overwrites the stack trace with the current location. Use the argument-less throw; to preserve the original stack trace.
using System;
static void Inner() => throw new Exception("Inner error");
static void Outer()
{
try { Inner(); }
catch (Exception e)
{
// NG: throw e; overwrites the stack trace with the Outer location.
throw e; // Information about Inner is lost.
}
}
static void OuterSafe()
{
try { Inner(); }
catch
{
// OK: throw; preserves the original stack trace.
throw;
}
}
Common Mistake 2: Throwing Exception Directly
Throwing the generic Exception class directly makes it impossible for callers to identify the type of error. Choose an appropriate exception class or define a custom one.
using System;
// NG: Generic Exception gives callers no information about what went wrong.
static void ProcessNG(string input)
{
if (input == null)
throw new Exception("input is null."); // Nothing useful is conveyed by the type.
}
// OK: Use an exception class that matches the situation.
static void ProcessOK(string? input)
{
if (input == null)
throw new ArgumentNullException(nameof(input)); // Clearly indicates a null-argument error.
if (input.Length == 0)
throw new ArgumentException("input cannot be empty.", nameof(input));
}
try { ProcessOK(null); }
catch (ArgumentNullException e) { Console.WriteLine(e.Message); }
This produces the following output:
dotnet run Value cannot be null. (Parameter 'input')
Overview
A custom exception class is defined by inheriting from Exception (or another existing exception class). Adding application-specific error codes or extra information as properties enables more detailed error handling in the catch block.
When rethrowing, writing throw e; overwrites the stack trace with the current position. Use the argument-less throw; when you want to preserve the original stack trace.
For the exception-catching syntax, see try / catch / finally.
If you find any errors or copyright issues, please contact us.