C# Programming Tutorial 0/45 lessons ~6 min read Lesson 29

    Custom Exceptions

    Custom exceptions express domain failures — InsufficientFundsException, PatientNotFoundException — enabling callers to catch precisely without string matching.

    Course progress0%
    Focus
    10 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Custom exceptions express domain failures — InsufficientFundsException, PatientNotFoundException — enabling callers to catch precisely without string matching. Inherit from Exception or specialized bases like InvalidOperationException when appropriate.

    Include message, inner exception, and optional custom properties (ErrorCode). Serializable attribute legacy for remoting. Interviewers ask when custom vs standard exceptions and designing exception hierarchies without explosion.

    The story

    When a customer tries to withdraw more than their checking account balance, the core banking service throws InsufficientFundsException with structured properties — account ID, requested amount, and available balance — so the API middleware can return a precise 422 response and log actionable details without parsing error message strings.

    Understanding the topic

    Key concepts

    • Derive from Exception or ApplicationException (avoid latter).
    • Three constructors: default, message, message+inner.
    • Custom properties for ErrorCode, EntityId.
    • Sealed unless extensible hierarchy intended.
    • Document which methods throw which types.
    • Map to ProblemDetails extensions in APIs.

    Step-by-step explanation

    1. throw new DomainException('reason', inner);
    2. Catch filters on type or ErrorCode property.
    3. Middleware inspects exception type → HTTP status.
    4. Unit tests assert Throws from xUnit.
    5. Avoid deep inheritance — flat + ErrorCode often enough.
    6. Localization via message resources at catch boundary.

    Practical code example

    Domain exception with error code for API mapping:

    csharp
    namespace TechLearningPro.CustomExceptions;
    public sealed class InsufficientFundsException : Exception
    {
    public Guid AccountId { get; }
    public decimal Requested { get; }
    public decimal Available { get; }
    public InsufficientFundsException(Guid accountId, decimal requested, decimal available)
    : base(
    quot;Account {accountId} cannot withdraw {requested:C}; available {available:C}.")
    {
    AccountId = accountId;
    Requested = requested;
    Available = available;
    }
    }
    public static class WithdrawalGuard
    {
    public static void EnsureFunds(decimal balance, decimal amount, Guid accountId)
    {
    if (amount > balance)
    throw new InsufficientFundsException(accountId, amount, balance);
    }
    }

    Line-by-line code explanation

    • sealed class InsufficientFundsException : Exception defines a typed domain failure.
    • public Guid AccountId { get; } carries the account identifier for structured logging.
    • public decimal Requested { get; } and Available { get; } expose the amounts involved.
    • : base($"Account {accountId} cannot withdraw...") sets a human-readable message for operators.
    • AccountId = accountId assigns custom properties in the constructor body.
    • WithdrawalGuard.EnsureFunds(...) centralizes the balance check before debiting.
    • if (amount > balance) throw new InsufficientFundsException(...) throws only for genuine overdraft attempts.
    • throw new stops execution immediately — callers must catch or let middleware handle it.

    Key takeaway: Rich properties enable structured logging and ProblemDetails extensions without parsing Message.

    Real-world use

    Where you'll use this in production

    • Banking overdraft and limit violations.
    • Healthcare contraindication alerts.
    • Multi-tenant authorization failures.
    • Inventory shortfall on order placement.

    Best practices

    • Name with Exception suffix.
    • Provide meaningful messages for operators.
    • Include inner exception when wrapping.
    • Map each domain exception to HTTP status once in middleware.
    • Don't create exception per HTTP status only — use ProblemDetails.

    Common mistakes

    • Inheriting ApplicationException without reason.
    • Too many exception types differing only by message.
    • Serializable boilerplate on modern apps unnecessarily.
    • Throwing custom for non-exceptional validation.

    Advanced interview questions

    Q1BeginnerWhen create custom exception?
    When callers need typed handling or structured domain context.
    Q2BeginnerRequired constructors?
    Parameterless, string message, string+inner — enable serialization patterns.
    Q3IntermediateException hierarchy depth?
    Keep shallow; ErrorCode enum alternative for variants.
    Q4IntermediateTest custom exceptions?
    Assert.Throws(() => ...).
    Q5AdvancedDesign exception strategy for microservices.
    Domain exceptions in service; translate to ProblemDetails at API gateway; never leak internal types across boundaries.

    Summary

    Custom exceptions model typed domain failures. Add properties for structured error handling. Map to HTTP centrally in middleware. Keep hierarchy shallow and purposeful. Next: file I/O operations.

    Ready to mark this lesson complete?Track your journey across the entire course.