Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 13 – Exceptions and resource cleanup

Designing exceptions, guard clauses and results

Choose between C# exceptions, guard clauses, Try methods and result types for each failure, and design custom exceptions that carry what a handler needs.

  • Intermediate
  • 20 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: Guard clauses throw the built-in exception that names the caller's mistake · An expected failure is a result, a caller's mistake is an exception. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Choose the built-in exception type and throw helper for each kind of mistake a caller can make
  • Decide between an exception, a Try method and a result type for each failure of an operation
  • Write a custom exception type that carries the data its handlers need
  • Measure what a refusal costs as an exception and as a result, and make an ignored result a build warning
  • Document the exceptions a method throws with XML comments, without checked exceptions

Before you start

What you’ll learn · Before you start

What you will learn

  • Choose the built-in exception type and throw helper for each kind of mistake a caller can make
  • Decide between an exception, a Try method and a result type for each failure of an operation
  • Write a custom exception type that carries the data its handlers need
  • Measure what a refusal costs as an exception and as a result, and make an ignored result a build warning
  • Document the exceptions a method throws with XML comments, without checked exceptions

Before you start

On this page

A transfer of 2,500 rupees from a wallet that holds 1,800 must fail, and so must a transfer of -500 and a wallet paying itself. The negative amount and the self-payment are mistakes in the calling code; the low balance is an everyday outcome that the app must show its user. Designing exceptions means giving each failure the signal that fits it.

You will pick the built-in exception for each caller mistake, choose between an exception, a Try method and a result, and then design custom exceptions, result types and their documentation.

Guard clauses throw the built-in exception that names the caller’s mistake

Wallet.Transfer in guards.cs starts with guard clause: A check at the start of a method that returns or throws at once when an input is bad, so the code after it can assume the input is good and stays unindented. More in “if, switch statements and switch expressions”, one for each mistake a caller can make, and each throws a type that .NET already has. The program makes every mistake once:

guards.cs: one guard for each mistake C# · guards/guards.cs
// Each guard throws the built-in exception that names the caller's mistake.
var asha = new Wallet("W-17", 1800m);
var ravi = new Wallet("W-42", 0m);

Report("a transfer that fits", () => asha.Transfer(ravi, 500m));
Report("no wallet to pay", () => asha.Transfer(null!, 500m));
Report("a negative amount", () => asha.Transfer(ravi, -500m));
Report("an amount of zero", () => asha.Transfer(ravi, 0m));
Report("above the limit", () => asha.Transfer(ravi, 150_000m));
Report("a wallet paying itself", () => asha.Transfer(asha, 500m));
asha.Close();
Report("a closed wallet", () => asha.Transfer(ravi, 500m));

static void Report(string what, Action transfer)
{
    try
    {
        transfer();
        Console.WriteLine($"{what}: done");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"{what}: {e.GetType().Name}, parameter {e.ParamName}");
        Console.WriteLine($"  {e.Message.ReplaceLineEndings(" ")}");
    }
    catch (InvalidOperationException e)
    {
        Console.WriteLine($"{what}: {e.GetType().Name}");
        Console.WriteLine($"  {e.Message}");
    }
}

sealed class Wallet(string id, decimal balance)
{
    public const decimal Limit = 100_000m;

    public string Id { get; } = id;

    public decimal Balance { get; private set; } = balance;

    public bool IsClosed { get; private set; }

    public void Close() => IsClosed = true;

    public void Transfer(Wallet to, decimal amount)
    {
        ArgumentNullException.ThrowIfNull(to);
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
        ArgumentOutOfRangeException.ThrowIfGreaterThan(amount, Limit);
        if (ReferenceEquals(to, this))
        {
            throw new ArgumentException("A wallet cannot pay itself.", nameof(to));
        }

        if (IsClosed)
        {
            throw new InvalidOperationException($"Wallet {Id} is closed; open it again before a transfer.");
        }

        if (amount > Balance)
        {
            throw new InvalidOperationException($"The balance of {Balance} is less than {amount}.");
        }

        Balance -= amount;
        to.Balance += amount;
    }
}

Output

a transfer that fits: done
no wallet to pay: ArgumentNullException, parameter to
  Value cannot be null. (Parameter 'to')
a negative amount: ArgumentOutOfRangeException, parameter amount
  amount ('-500') must be a non-negative and non-zero value. (Parameter 'amount') Actual value was -500.
an amount of zero: ArgumentOutOfRangeException, parameter amount
  amount ('0') must be a non-negative and non-zero value. (Parameter 'amount') Actual value was 0.
above the limit: ArgumentOutOfRangeException, parameter amount
  amount ('150000') must be less than or equal to '100000'. (Parameter 'amount') Actual value was 150000.
a wallet paying itself: ArgumentException, parameter to
  A wallet cannot pay itself. (Parameter 'to')
a closed wallet: InvalidOperationException
  Wallet W-17 is closed; open it again before a transfer.

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run guards.cs

The throw helper: A static method that checks an argument and throws the matching exception with a complete message, such as ArgumentNullException.ThrowIfNull or ArgumentOutOfRangeException.ThrowIfNegativeOrZero. wrote the messages, with the parameter’s name and the refused value: ThrowIfNegativeOrZero and ThrowIfGreaterThan, new in .NET 8, refused -500, 0 and 150000, and ArgumentNullException.ThrowIfNull the missing wallet. A wallet paying itself has no helper, so Transfer throws ArgumentException with nameof(to). A closed wallet is a call at the wrong moment, not a bad argument: InvalidOperationException.

The caller Throw In guards.cs
passed null ArgumentNullException, with ThrowIfNull no wallet to pay
passed a value out of range ArgumentOutOfRangeException, with ThrowIfNegativeOrZero, ThrowIfGreaterThan … -500, 0 and 150000
passed a value that is wrong in another way ArgumentException, with nameof a wallet paying itself
called when the object’s state forbids it InvalidOperationException a closed wallet

NotSupportedException says that an object does not support the member called, as Write on a read-only stream. Never throw Exception, SystemException, NullReferenceException or IndexOutOfRangeException on purpose: the first two tell a handler nothing it can act on, and the runtime throws the other two for its own checks. Because guard clauses run first, a bad call fails before any money has moved.

Checkpoint

Check: Guard clauses and the built-in exception for each mistake

  1. Question 1 of 2 Which helper refuses an amount of zero as well as a negative amount?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount)

    The ArgumentOutOfRangeException helpers of .NET 8 name the parameter and the refused value for you: ThrowIfNegativeOrZero, ThrowIfGreaterThan and the others.

  2. Question 2 of 2 Why does Transfer throw InvalidOperationException, not ArgumentException, when the wallet is closed?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Every argument is valid; it is the state of the wallet that forbids the call

    Bad arguments get ArgumentException or a type derived from it; a call that the object's current state forbids gets InvalidOperationException.

An expected failure is a result, a caller’s mistake is an exception

guards.cs also throws for a low balance, its one doubtful choice: careful users meet a low balance every day. three_ways.cs offers the transfer three ways: Transfer throws, TryTransfer returns false, and TransferOrRefuse returns a result type: A type that a method returns to report an outcome as a value: success with its data, or failure with its reason, so that the caller branches on it instead of catching an exception., TransferResult, done or refused with its reason:

three_ways.cs: one transfer, three signals C# · three-ways/three_ways.cs
// One transfer offered three ways: an exception, a Try method and a result.
var asha = new Wallet("W-17", 1800m);
var ravi = new Wallet("W-42", 0m);

// 1. An exception: the caller must catch it, or the program stops.
try
{
    asha.Transfer(ravi, 2500m);
}
catch (InvalidOperationException e)
{
    Console.WriteLine($"Transfer threw {e.GetType().Name}: {e.Message}");
}

// 2. A Try method: true or false, and nothing more.
bool moved = asha.TryTransfer(ravi, 2500m);
Console.WriteLine($"TryTransfer returned {moved}");

// 3. A result: the outcome and its reason are values the caller reads.
Show(asha.TransferOrRefuse(ravi, 2500m));
Show(asha.TransferOrRefuse(ravi, 500m));
asha.Freeze();
Show(asha.TransferOrRefuse(ravi, 500m));

// Nothing makes a caller look at a Try method's bool or at a result: these two lines compile without a warning.
asha.TryTransfer(ravi, 100m);
asha.TransferOrRefuse(ravi, 100m);
Console.WriteLine($"balances: W-17 {asha.Balance}, W-42 {ravi.Balance}");

static void Show(TransferResult result) =>
    Console.WriteLine(result.Succeeded ? $"TransferOrRefuse: done, balance {result.Balance}" : $"TransferOrRefuse: refused, {result.Refusal}");

sealed class Wallet(string id, decimal balance)
{
    public string Id { get; } = id;

    public decimal Balance { get; private set; } = balance;

    public bool IsFrozen { get; private set; }

    public void Freeze() => IsFrozen = true;

    // The exception form: a refusal becomes InvalidOperationException.
    public void Transfer(Wallet to, decimal amount)
    {
        TransferResult result = TransferOrRefuse(to, amount);
        if (!result.Succeeded)
        {
            throw new InvalidOperationException(result.Refusal);
        }
    }

    // The Try form: the reason is lost.
    public bool TryTransfer(Wallet to, decimal amount) => TransferOrRefuse(to, amount).Succeeded;

    // The result form. A caller's mistake still throws: only refusals are results.
    public TransferResult TransferOrRefuse(Wallet to, decimal amount)
    {
        ArgumentNullException.ThrowIfNull(to);
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
        if (IsFrozen)
        {
            return TransferResult.Refused($"wallet {Id} is frozen", Balance);
        }

        if (amount > Balance)
        {
            return TransferResult.Refused($"the balance of {Balance} is less than {amount}", Balance);
        }

        Balance -= amount;
        to.Balance += amount;
        return TransferResult.Done(Balance);
    }
}

readonly record struct TransferResult(bool Succeeded, decimal Balance, string? Refusal)
{
    public static TransferResult Done(decimal balance) => new(true, balance, null);

    public static TransferResult Refused(string reason, decimal balance) => new(false, balance, reason);
}

Output

Transfer threw InvalidOperationException: the balance of 1800 is less than 2500
TryTransfer returned False
TransferOrRefuse: refused, the balance of 1800 is less than 2500
TransferOrRefuse: done, balance 1300
TransferOrRefuse: refused, wallet W-17 is frozen
balances: W-17 1300, W-42 500

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run three_ways.cs

All three refused the 2,500, but the callers learned different things. The exception carried the reason, at the cost of a throw and a catch block; the Try method said only False; the result said why, as a value, and named the frozen wallet too. The last lines show the risk of the quiet forms: nothing makes a caller read a bool or a result, and two refusals went by without a word.

What differs Exception Try method Result
The caller learns the type, the message, the stack trace success or failure what the result type holds
If the caller ignores it the program stops at the top level the failure passes silently the failure passes silently
A failure costs a throw and a search for a handler a branch a branch
It fits a caller’s mistake, a failing environment one obvious reason, as in TryParse an outcome to show or to act on

Results suit outcomes a caller must act on. Exceptions keep their place inside a method that returns results: TransferOrRefuse still throws for a missing wallet or an amount of zero, which are bugs in the caller. That split is the core of designing exceptions. Answer three questions about a failure of your own:

Interactive · answer and compare Which signal fits this failure? Answer three questions about one failure of your method. The table rules out the signals that would report it badly.
Is the failure a mistake in the calling code, such as an amount of zero?
Does it happen in normal use, many times a day?
Must the caller learn why it failed, to tell a user or to decide what to do next?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

An exception, a Try method and a result, against four things that decide how a failure is reported
Signal A caller's mistake Happens in normal use Caller needs the reason If the caller ignores it
An exception Caller writes: try and catch, or nothing at all Good stops the call at once and names the parameter, as in guards.cs Poor every refusal pays for a throw and a catch Good the type and the message say why Good cannot pass silently: one that nothing catches stops the program
A Try method Caller writes: if (!TryTransfer(…)) Poor a false is easy to miss, and the bug goes on Good a branch, with no throw Poor only true or false: TryTransfer said False Poor an unread false passes silently
A result type Caller writes: if (!result.Succeeded) Poor a refused result is as easy to miss as a false Good a branch and a small value Good carries it: wallet W-17 is frozen Poor an unread result passes silently, as in three_ways.cs
How the answers decide
  • “Is the failure a mistake in the calling code, such as an amount of zero?” Yes, correct code never does this: rules out A Try method and A result type. A bug in the calling code should stop the call before anything changes. A false or a refused result can be ignored, and the bug lives on.
  • “Is the failure a mistake in the calling code, such as an amount of zero?” No, it can happen to a careful caller and “Does it happen in normal use, many times a day?” Yes, like a low balance: rules out An exception. An outcome that happens every day would cost a throw and a catch each time, and fill the logs with failures that are not errors.
  • “Is the failure a mistake in the calling code, such as an amount of zero?” No, it can happen to a careful caller and “Must the caller learn why it failed, to tell a user or to decide what to do next?” Yes, there is more than one reason: rules out A Try method. A Try method returns only true or false, so the reason is lost: TryTransfer said False for a balance that was too low.
  • “Is the failure a mistake in the calling code, such as an amount of zero?” No, it can happen to a careful caller and “Must the caller learn why it failed, to tell a user or to decide what to do next?” No, failure has one obvious meaning: adds a caveat to A result type. A result works, but when failure has one obvious meaning, as in int.TryParse, a bool and an out value say it with less code.
  • “Is the failure a mistake in the calling code, such as an amount of zero?” No, it can happen to a careful caller: adds a caveat to A Try method and A result type. Both pass silently when a caller ignores them, so name them so that reading the outcome is the obvious thing to do.

In short, guard clauses throw for the caller’s mistakes, results report what a user must hear, and a Try method serves when a failure has one obvious meaning.

Key idea

Throw for a mistake in the calling code, return a result for an outcome that careful callers meet every day, and use a Try method when the only question is whether it worked.

Checkpoint

Check: An exception, a Try method or a result

  1. Question 1 of 2 At the end of three_ways.cs, TryTransfer and TransferOrRefuse are called on the frozen wallet, and their outcomes are not read. What happened?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Both were refused, nothing moved, and nothing reported it: the balances stayed 1300 and 500

    The quiet forms are cheap and carry outcomes, but a caller can drop them, which is why a caller's mistake is still an exception.

  2. Question 2 of 2 A seat-booking method meets two failures at a sale: the seat was booked by someone else a second ago, hundreds of times a minute, and a request with seat number -3. Which signals fit?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A refused result for the seat already taken, and ArgumentOutOfRangeException for -3

    Ask for each failure whether careful callers meet it in normal use; if they do, return it, and if only buggy callers do, throw.

Custom exceptions that carry the data a handler needs Pro: opens with a Pro, Premium or Ultimate pass

When no built-in type fits and a handler must act on the details, a custom exception carries them as properties. This part writes one, wraps a cause with it, and builds the serialization members that .NET 8 made obsolete.

In this part:

  • Writing InsufficientBalanceException, and a filter that reads its data
  • Wrapping the cause, and adding context on the way up
  • Serialization members that .NET 8 made obsolete, built both ways

3 runnable examples (C#, Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Result types in depth: reasons, retries and the measured cost Pro: opens with a Pro, Premium or Ultimate pass

A result type is only as useful as the reasons it can name and the callers that read it. This part designs one for transfers, measures it against exceptions, and makes the build warn when a result is ignored.

In this part:

  • A reason the caller switches on, and a conflict it retries
  • 100,000 refusals, measured as exceptions and as results
  • Making an ignored result a build warning

3 runnable examples (C#, Python) · about 4 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Documenting what a method throws, without checked exceptions Pro: opens with a Pro, Premium or Ultimate pass

In C# a method’s exceptions are part of its documentation, not of its signature. This part shows why, writes the documentation that the compiler checks, and shows what the callers of task-returning methods receive.

In this part:

  • Why C# leaves exceptions out of the signature
  • XML exception tags, checked by the compiler
  • What callers of task-returning methods receive

3 runnable examples (C#, Python) · about 5 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Practise: the error model of a wallet transfer

transfer.cs checks a TransferOrRefuse that you write, with the rules at the top of the file. As it starts, the method throws NotImplementedException, so all eight checks fail:

transfer.cs: eight checks of TransferOrRefuse C# · transfer/transfer.cs
// Practice: write TransferOrRefuse, then run this file until all eight checks pass.
// The rules: a caller's mistake throws the built-in exception for it (no wallet to pay, an amount of zero or less, a
// wallet paying itself); an outcome the user must be told about is a refused result with the reason three_ways.cs
// gives ("wallet W-17 is frozen", or "the balance of 1800 is less than 2500" for a balance that is too low), and it
// leaves both balances as they were; every other transfer moves the money and is done.
(string Check, Func<string> Run, string Expected)[] checks =
[
    ("a transfer that fits", () => Describe(Pay(1800m, 500m)), "done, balance 1300"),
    ("more than the balance", () => Describe(Pay(1800m, 2500m)), "refused: the balance of 1800 is less than 2500"),
    ("a frozen wallet", () => Describe(Pay(1800m, 500m, frozen: true)), "refused: wallet W-17 is frozen"),
    ("a refusal moves no money", () => BalancesAfterRefusal(), "1800 and 0"),
    ("no wallet to pay", () => Describe(new Wallet("W-17", 1800m).TransferOrRefuse(null!, 500m)), "throws ArgumentNullException (to)"),
    ("a negative amount", () => Describe(Pay(1800m, -500m)), "throws ArgumentOutOfRangeException (amount)"),
    ("an amount of zero", () => Describe(Pay(1800m, 0m)), "throws ArgumentOutOfRangeException (amount)"),
    ("a wallet paying itself", () => PayItself(), "throws ArgumentException (to)"),
];

int passed = 0;
foreach (var (check, run, expected) in checks)
{
    string got;
    try
    {
        got = run();
    }
    catch (ArgumentException e)
    {
        got = $"throws {e.GetType().Name} ({e.ParamName})";
    }
    catch (Exception e)
    {
        got = $"throws {e.GetType().Name}";
    }

    bool ok = got == expected;
    if (ok)
    {
        passed++;
    }

    Console.WriteLine(ok ? $"{check}: ok" : $"{check}: FAIL, {got} instead of {expected}");
}

Console.WriteLine($"{passed} of {checks.Length} checks pass");
return passed == checks.Length ? 0 : 1;

static TransferResult Pay(decimal balance, decimal amount, bool frozen = false)
{
    var from = new Wallet("W-17", balance);
    if (frozen)
    {
        from.Freeze();
    }

    return from.TransferOrRefuse(new Wallet("W-42", 0m), amount);
}

static string BalancesAfterRefusal()
{
    var from = new Wallet("W-17", 1800m);
    var to = new Wallet("W-42", 0m);
    from.TransferOrRefuse(to, 2500m);
    return $"{from.Balance} and {to.Balance}";
}

static string PayItself()
{
    var wallet = new Wallet("W-17", 1800m);
    return Describe(wallet.TransferOrRefuse(wallet, 500m));
}

static string Describe(TransferResult result) =>
    result.Succeeded ? $"done, balance {result.Balance}" : $"refused: {result.Refusal}";

sealed class Wallet(string id, decimal balance)
{
    public string Id { get; } = id;

    public decimal Balance { get; private set; } = balance;

    public bool IsFrozen { get; private set; }

    public void Freeze() => IsFrozen = true;

    public TransferResult TransferOrRefuse(Wallet to, decimal amount)
    {
        throw new NotImplementedException();
    }
}

readonly record struct TransferResult(bool Succeeded, decimal Balance, string? Refusal)
{
    public static TransferResult Done(decimal balance) => new(true, balance, null);

    public static TransferResult Refused(string reason, decimal balance) => new(false, balance, reason);
}

Output (exit status 1)

a transfer that fits: FAIL, throws NotImplementedException instead of done, balance 1300
more than the balance: FAIL, throws NotImplementedException instead of refused: the balance of 1800 is less than 2500
a frozen wallet: FAIL, throws NotImplementedException instead of refused: wallet W-17 is frozen
a refusal moves no money: FAIL, throws NotImplementedException instead of 1800 and 0
no wallet to pay: FAIL, throws NotImplementedException instead of throws ArgumentNullException (to)
a negative amount: FAIL, throws NotImplementedException instead of throws ArgumentOutOfRangeException (amount)
an amount of zero: FAIL, throws NotImplementedException instead of throws ArgumentOutOfRangeException (amount)
a wallet paying itself: FAIL, throws NotImplementedException instead of throws ArgumentException (to)
0 of 8 checks pass

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run transfer.cs

Make transfer.cs pass

Copy transfer.cs, and give TransferOrRefuse its guards first, then the two refusals, then the move. Run dotnet run transfer.cs until it prints 8 of 8 checks pass.

The self-check below covers the two sections above the in-depth parts, and every choice says what it gets right or misses.

Practice that adapts to you

Practise this lesson

7 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 7 Which exception type should your own code never throw on purpose?

    Choose one answer.

    Show the answer: Question 1 of 7

    Answer: NullReferenceException

    Never throw Exception, SystemException, NullReferenceException or IndexOutOfRangeException from your own code.

  2. Question 2 of 7 ArgumentOutOfRangeException.ThrowIfGreaterThan(amount, Limit) refused 150000 in guards.cs. What did its message tell the reader?

    Choose one answer.

    Show the answer: Question 2 of 7

    Answer: The parameter name amount, the limit 100000 and the actual value 150000

    The throw helpers name the parameter and the refused value, so the message points straight at the mistake.

  3. Question 3 of 7 Why do the guard clauses of Transfer come before the lines that change the balances?

    Choose one answer.

    Show the answer: Question 3 of 7

    Answer: So that a bad call fails before anything has changed, and no half-done transfer needs undoing

    Check every argument and the object's state first, then do the work: a failure then leaves nothing to clean up.

  4. Question 4 of 7 What did TryTransfer report for the transfer of 2,500 from a wallet holding 1,800?

    Choose one answer.

    Show the answer: Question 4 of 7

    Answer: False, and nothing about why

    A Try method fits when a failure has one obvious meaning; when the caller needs the reason, return a result.

  5. Question 5 of 7 TransferOrRefuse returns a TransferResult. What does it do when it is called with an amount of zero?

    Choose one answer.

    Show the answer: Question 5 of 7

    Answer: It throws ArgumentOutOfRangeException, because zero is a mistake in the calling code

    A method that returns results for outcomes still throws for its callers' mistakes.

  6. Question 6 of 7 Why is an exception a poor fit for a low balance in a busy payments app?

    Choose one answer.

    Show the answer: Question 6 of 7

    Answer: It happens in normal use, so every refusal would pay for a throw and a catch, and logs would fill with failures that are not errors

    Exceptions are for what should not happen; a low balance happens all day.

  7. Question 7 of 7 A library has int ParseRollNumber(string text), which throws for bad text. A search box now needs the same parsing for what students type, and about 3 in 100 inputs are mistyped. What do you add?

    Choose one answer.

    Show the answer: Question 7 of 7

    Answer: bool TryParseRollNumber(string text, out int roll), and keep ParseRollNumber for data that must be valid

    Offer a Try method beside a throwing method when the same failure is routine for some callers and a bug for others, as int.Parse and int.TryParse do.

Interview questions

Fresher · When should a method throw an exception, and when should it return a failure?

Throw when the method cannot do what its name promises because of something that should not happen: a mistake in the calling code, such as a null argument or an amount of zero, or a failing environment, such as a lost connection. An exception cannot be ignored, and it stops the work before anything changes. Return a failure, false from a Try method or a result value, when the outcome is part of normal use: a low balance, a mistyped number, a seat someone just booked. Those should cost a branch, not a throw. A method may do both: throw for bad arguments and return results for refusals.

  1. Mid to senior

    A wallet service throws InsufficientFundsException whenever a balance is too low. At peak it throws 40,000 of them an hour, and the team believes exceptions are slowing it down. Would you change the design? How do you decide, and how do you measure the effect?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior to staff

    Design the error model of a money-transfer API that a mobile app and other internal services call over HTTP. Which failures are exceptions inside the service, which are results, and what does each kind of caller receive?

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    A team that moved from Java wants every public method of a C# library to declare what it throws, with a build step that fails whenever a method can throw something new. What do you recommend instead, and why?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Guard clauses throw the built-in type for each caller mistake, with throw helpers such as ThrowIfNegativeOrZero, and InvalidOperationException when the object’s state forbids the call.
  • A failure that careful callers meet in normal use, such as a low balance, is a result or a false, while a mistake in the calling code is an exception.
  • A Try method loses the reason, a result carries it, and nothing makes a caller read either one.

Terms in this lesson

result type
A type that a method returns to report an outcome as a value: success with its data, or failure with its reason, so that the caller branches on it instead of catching an exception.
throw helper
A static method that checks an argument and throws the matching exception with a complete message, such as ArgumentNullException.ThrowIfNull or ArgumentOutOfRangeException.ThrowIfNegativeOrZero.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.