Modern C# · Lesson 13

Guard clauses and precise exceptions

Reject invalid inputs before they reach the main calculation.

How it works

A method boundary is a good place to explain which inputs make sense. Price accepts a nonnegative count and a nonnegative unit price. Negative numbers are legal integers, but they do not satisfy this method's contract. Guard clauses make those restrictions visible before the successful calculation begins.

The first call passes both guards and produces twelve. The second call stops at the count guard. An ArgumentOutOfRangeException identifies an argument whose value lies outside the accepted domain. nameof(count) produces the parameter name without duplicating its spelling in an unrelated string literal. Renaming the parameter updates that expression.

Notice that the exception is caught outside Price. The calculation should not turn its own validation failure into a plausible price such as zero. Zero already means a valid empty order. Conflating those outcomes would make callers guess whether a calculation succeeded, and an incorrect total could travel much farther through the application.

The checked expression handles a different boundary: multiplication can exceed the range of an integer even when both operands are nonnegative. Domain validation and numeric overflow are separate concerns. checked does not decide the business rule, and guard clauses do not automatically make every arithmetic operation safe.

Practice by adding a maximum count of one thousand and including the rejected value in the exception constructor. Test zero, the maximum, one above the maximum, a negative count, and a multiplication that overflows. Predict which guard or calculation fails for each case before running your changes.

Finally, decide what callers should catch. A user interface can translate an expected validation error into a helpful message, while an unexpected exception should remain distinguishable for diagnosis. Catching Exception around every calculation and returning zero would erase both that distinction and the evidence needed to repair a bug.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

Console.WriteLine(Price(3, 4));
try { Price(-1, 4); }
catch (ArgumentOutOfRangeException e) { Console.WriteLine(e.ParamName); }
static int Price(int count, int unit) {
    if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
    if (unit < 0) throw new ArgumentOutOfRangeException(nameof(unit));
    return checked(count * unit);
}

Expected output

12
count
Try this
Validate each independent argument before doing arithmetic.