Guard Clauses
Overview
Suppose a shipping function rejects empty orders, offers free shipping to members and orders worth at least 50, and charges 5 otherwise. If each condition opens an if and nests the next check inside it, the common case, a shipping charge of 5, ends up three levels deep. Rejecting an empty order is pushed into the final else. Other branches separate a condition from its outcome, so you have to count indentation to work out which else belongs to which if. The example below calls this version ShippingNested.
A guard clause checks a special case or failure condition near the start of a function and immediately returns or throws when that condition holds. The code remaining after all guards is the normal path, where the function does its intended work. Each guard keeps its outcome immediately below its condition, and with no else branches, the normal path stays at one indentation level.
Guards handle two kinds of failure. Calling code that breaks a method's contract, for example by passing null, contains a bug, so the method throws. A user submitting an empty reason or trying to cancel an order that has already shipped is an expected operational failure. The method returns that failure, with an error code, in a result type.
Structure
This UML activity diagram follows order cancellation in the extended example. Diamonds are decision nodes; brackets contain the condition for following a flow. A request stopped by a guard leaves downward, returns an Err with an error code, and ends. Only requests that follow [else] through every guard reach cancellation on the right. The null checks that throw exceptions are omitted.
History
Kent Beck included Guard Clause in Smalltalk Best Practice Patterns in 1996. The publisher's table of contents places it in Chapter 7, Formatting, between Rectangular Block and Conditional Expression.1
Beck revisited the pattern in Chapter 7, Behavior, of Implementation Patterns (2007). The chapter's opening summary describes guard clauses as expressing local exceptional flows through early returns.2 The section distinguishes if-then-else, which expresses two equally important flows, from a guard clause, which suits a situation where one flow matters more than the other. Beck calls guards especially useful with multiple conditions and writes that nested conditionals lead to defects.3
Martin Fowler's refactoring catalog includes Replace Nested Conditional with Guard Clauses. The catalog accompanies the second edition of Refactoring, published in 2018. Its before-and-after example changes getPayAmount from nested if-else branches calculating a payment into immediate returns for each condition. This unfolds a function that returns different outcomes into cases that finish early and the case that remains to the end.4
The single-exit rule
Guard clauses conflict with the old rule that a routine should have one entry and one exit. In the same section, Beck explains that the rule sought to prevent the confusion caused by jumping into and out of several locations within one routine. He considers it reasonable for FORTRAN or assembly programs with extensive global data, where even knowing which statements had run was difficult, but unnecessarily conservative for Java methods that are small and mostly use local variables.5
Another concern is that cleanup needs a single exit to run reliably. In C#, a return that leaves a try block still runs its finally block. The compiler also translates a using statement into try-finally, so early returns still perform resource cleanup.6
Kinds of guards and their order
Guards against programming errors throw exceptions. The .NET Framework Design Guidelines say that an invalid argument passed to a member should cause an ArgumentException or one of its subtypes.7 Helpers express some checks in one line: ArgumentNullException.ThrowIfNull has been available since .NET 6, ArgumentException.ThrowIfNullOrEmpty since .NET 7, and ArgumentOutOfRangeException.ThrowIfNegative since .NET 8. When its parameter name is omitted, ThrowIfNull uses the CallerArgumentExpression attribute to include the text of the argument expression in the exception. In the extended example that text is command.8
Guards for expected business failures return a result type. The same guidelines say to report failures through exceptions rather than error codes, but also advise avoiding exceptions for normal control flow where possible. For members that may fail in common situations, they suggest avoiding exception-related performance costs through Tester-Doer, which provides a separate member to check a condition, or Try-Parse, which returns a Boolean success flag and supplies the result through an out parameter.9 Like Try-Parse, Result reports common failures in the return value, but it also includes an error code explaining the failure. It therefore still differs from the guideline against returning error codes. The reasoning and costs of this choice are covered in Result Type.
A guard must expose failure rather than silently repair input to let it pass. Replacing an empty reason with a default such as "no reason given" lets the caller believe the request was accepted as submitted; the record later obscures who cancelled without a reason. If filling a default is an actual business rule, put that operation on the normal path rather than in a guard.
The extended example checks input that can be judged without accessing storage, loads the target, checks authorization, and then checks business rules. Cheap checks come first so requests that will fail do not incur a lookup. Authorization precedes business rules to limit information disclosure. If bob tries to cancel alice's shipped order and the business rule runs first, bob receives AlreadyShipped and learns that someone else's order has shipped.
Why use it?
The normal path stays at one indentation level, and every failure condition sits beside its outcome. Adding or removing a guard leaves the indentation of other branches alone. Distinct error codes reveal which check stopped a request, and tests can pair an input with each guard.
Guard clauses leave the number of checks unchanged. When a function has too many conditions, move validation into a separate function or policy object. As Beck's distinction suggests, if-then-else better fits two equally important paths. Guards stop at the first failure, so a form that must display every error needs to run all checks and collect their failures instead. A guard also sees state only at the instant of the check. If another request can change that state before the update, a transaction or conditional update must separately handle concurrency.
Example
The shipping calculation from the overview is written once with nested if statements and once with guard clauses. Both functions receive the same four inputs so their results can be compared.
using System;
using System.Globalization;
var orders = new (int Items, decimal Subtotal, bool Member)[]
{
(0, 0m, false),
(2, 30m, true),
(3, 80m, false),
(1, 20m, false),
};
foreach (var (items, subtotal, member) in orders)
{
var nested = ShippingNested(items, subtotal, member);
var guarded = ShippingGuarded(items, subtotal, member);
var who = member ? "member" : "guest";
var amount = subtotal.ToString(CultureInfo.InvariantCulture);
Console.WriteLine($"items={items} subtotal={amount} {who}: {guarded} (same: {nested == guarded})");
}
// Nested if: the normal path, the default shipping charge, is deepest.
static string ShippingNested(int items, decimal subtotal, bool member)
{
string result;
if (items > 0)
{
if (!member)
{
if (subtotal < 50m)
{
result = "fee 5";
}
else
{
result = "free (over 50)";
}
}
else
{
result = "free (member)";
}
}
else
{
result = "rejected: empty order";
}
return result;
}
// Guard clauses: handle special cases first and return immediately.
static string ShippingGuarded(int items, decimal subtotal, bool member)
{
if (items <= 0) return "rejected: empty order";
if (member) return "free (member)";
if (subtotal >= 50m) return "free (over 50)";
return "fee 5";
}items=0 subtotal=0 guest: rejected: empty order (same: True)
items=2 subtotal=30 member: free (member) (same: True)
items=3 subtotal=80 guest: free (over 50) (same: True)
items=1 subtotal=20 guest: fee 5 (same: True)Both functions return the same value for all four inputs. In the guarded version, rejecting an empty order comes first, and the default charge of 5 stays on the final line at the function body's outer indentation level.
Extended example
This example handles an order-cancellation command. A command handler receives a request such as "cancel this order", validates it, and changes state; it is where the guards gather here. A null command or user triggers an exception through ThrowIfNull. Other failures return an error code (ErrorCode) and the stage that failed (Stage) in a Result. Seven requests are sent to the same handler in sequence, followed by a null command.
using System;
using System.Collections.Generic;
var orders = new Dictionary<string, Order>
{
["order-1"] = new Order("alice", OrderStatus.Placed),
["order-2"] = new Order("alice", OrderStatus.Shipped),
["order-3"] = new Order("alice", OrderStatus.Placed),
};
var alice = new User("alice", IsAdmin: false);
var bob = new User("bob", IsAdmin: false);
var carol = new User("carol", IsAdmin: true);
var handler = new CancelOrderHandler(orders);
var requests = new (CancelOrder Command, User Actor)[]
{
(new CancelOrder("order-1", "changed mind"), alice),
(new CancelOrder("order-1", "changed mind"), alice),
(new CancelOrder("order-2", "too slow"), bob),
(new CancelOrder("order-2", "customer call"), carol),
(new CancelOrder("order-9", "typo"), alice),
(new CancelOrder("order-3", " "), alice),
(new CancelOrder("order-3", null), alice),
};
foreach (var (command, actor) in requests)
{
var result = handler.Handle(command, actor);
var text = result.Failure is { } f ? $"Err {f.Code} at {f.Stage}" : $"Ok {result.Value}";
Console.WriteLine($"{actor.Id} cancels {command.OrderId}: {text}");
}
// A null command is a bug in the caller. Only the demo catches and prints it.
try
{
handler.Handle(null!, alice);
}
catch (ArgumentNullException ex)
{
Console.WriteLine($"bug: {ex.GetType().Name} ({ex.ParamName})");
}
enum OrderStatus { Placed, Shipped, Cancelled }
enum ErrorCode { ReasonMissing, OrderNotFound, PermissionDenied, AlreadyShipped, AlreadyCancelled }
enum Stage { Validate, Load, Authorize, Rule }
sealed record User(string Id, bool IsAdmin);
sealed record CancelOrder(string OrderId, string? Reason);
sealed class Order(string ownerId, OrderStatus status)
{
public string OwnerId { get; } = ownerId;
public OrderStatus Status { get; set; } = status;
public string? CancelReason { get; set; }
}
sealed record Failure(ErrorCode Code, Stage Stage);
// A null Failure means success. If T is a value type, a failed result has default(T) in Value.
sealed record Result<T>(T? Value, Failure? Failure)
{
public static Result<T> Ok(T value) => new(value, null);
public static Result<T> Fail(ErrorCode code, Stage stage) => new(default, new Failure(code, stage));
}
sealed class CancelOrderHandler(Dictionary<string, Order> orders)
{
public Result<OrderStatus> Handle(CancelOrder command, User actor)
{
// A contract violation is a programming error, so throw an exception.
ArgumentNullException.ThrowIfNull(command);
ArgumentNullException.ThrowIfNull(actor);
// 1. Validate input without accessing storage.
// Reject an empty reason rather than replacing it with a default.
if (string.IsNullOrWhiteSpace(command.Reason))
return Result<OrderStatus>.Fail(ErrorCode.ReasonMissing, Stage.Validate);
// 2. Load the target
if (!orders.TryGetValue(command.OrderId, out var order))
return Result<OrderStatus>.Fail(ErrorCode.OrderNotFound, Stage.Load);
// 3. Check authorization before revealing order state.
if (order.OwnerId != actor.Id && !actor.IsAdmin)
return Result<OrderStatus>.Fail(ErrorCode.PermissionDenied, Stage.Authorize);
// 4. Business rules
if (order.Status == OrderStatus.Shipped)
return Result<OrderStatus>.Fail(ErrorCode.AlreadyShipped, Stage.Rule);
if (order.Status == OrderStatus.Cancelled)
return Result<OrderStatus>.Fail(ErrorCode.AlreadyCancelled, Stage.Rule);
// Normal path: only requests passing every guard reach here.
order.Status = OrderStatus.Cancelled;
order.CancelReason = command.Reason;
return Result<OrderStatus>.Ok(order.Status);
}
}alice cancels order-1: Ok Cancelled
alice cancels order-1: Err AlreadyCancelled at Rule
bob cancels order-2: Err PermissionDenied at Authorize
carol cancels order-2: Err AlreadyShipped at Rule
alice cancels order-9: Err OrderNotFound at Load
alice cancels order-3: Err ReasonMissing at Validate
alice cancels order-3: Err ReasonMissing at Validate
bug: ArgumentNullException (command)The first request cancels the order, so repeating it returns AlreadyCancelled. Requests with a whitespace-only reason and those with no reason both return ReasonMissing. Because lookup precedes authorization, someone testing another user's order number can distinguish an existing order from an absent one through OrderNotFound and PermissionDenied. Return the same error code for both cases if existence must also be hidden. The handler changes an in-memory dictionary on one thread and does not prevent concurrent requests for the same order. In a database, combine the state check and update into one conditional operation, such as changing Placed to Cancelled only when the current state is Placed. This example's Result identifies success by whether Failure is null. When T is a value type such as OrderStatus, even a failed result's Value contains the default value, Placed. Callers must therefore check Failure first. The record also has a public constructor, allowing callers to create a result with both fields populated.
TypeScript example
The TypeScript compiler narrows a variable's type in code after a guard returns. The handbook calls refining a declared type into a more specific type narrowing. Control-flow analysis recognizes that the code after a return is unreachable for some types.10 Below, this.orders.get() returns Order | undefined, but after the order === undefined guard, its value is treated as an Order. The result type is a discriminated union whose ok field distinguishes the cases. The caller's if (!result.ok) { ...; continue; } also acts as a guard, allowing code below it to use result.value. Beck lists continue inside a loop as a variation of a guard clause.11 The final null command from the C# example is omitted. With --strict, passing null where a CancelOrder is required fails to compile.
type OrderStatus = "Placed" | "Shipped" | "Cancelled";
type ErrorCode = "ReasonMissing" | "OrderNotFound" | "PermissionDenied" | "AlreadyShipped" | "AlreadyCancelled";
type Stage = "Validate" | "Load" | "Authorize" | "Rule";
interface User { readonly id: string; readonly isAdmin: boolean; }
interface CancelOrder { readonly orderId: string; readonly reason?: string; }
interface Order { readonly ownerId: string; status: OrderStatus; cancelReason: string | null; }
// A discriminated union using ok to distinguish success and failure
type Result<T> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly code: ErrorCode; readonly stage: Stage };
const fail = (code: ErrorCode, stage: Stage): Result<never> => ({ ok: false, code, stage });
class CancelOrderHandler {
constructor(private readonly orders: Map<string, Order>) {}
handle(command: CancelOrder, actor: User): Result<OrderStatus> {
const reason = command.reason; // string | undefined
// 1. Validate input: reject an empty reason rather than defaulting it.
if (reason === undefined || reason.trim() === "") return fail("ReasonMissing", "Validate");
// From here, reason is narrowed to string.
const order = this.orders.get(command.orderId); // Order | undefined
// 2. Load the target
if (order === undefined) return fail("OrderNotFound", "Load");
// From here, order is narrowed to Order.
// 3. Check authorization before revealing order state.
if (order.ownerId !== actor.id && !actor.isAdmin) return fail("PermissionDenied", "Authorize");
// 4. Business rules
if (order.status === "Shipped") return fail("AlreadyShipped", "Rule");
if (order.status === "Cancelled") return fail("AlreadyCancelled", "Rule");
// Normal path: reason is string and order is Order; no assertions are needed.
order.status = "Cancelled";
order.cancelReason = reason;
return { ok: true, value: order.status };
}
}
const orders = new Map<string, Order>([
["order-1", { ownerId: "alice", status: "Placed", cancelReason: null }],
["order-2", { ownerId: "alice", status: "Shipped", cancelReason: null }],
["order-3", { ownerId: "alice", status: "Placed", cancelReason: null }],
]);
const alice: User = { id: "alice", isAdmin: false };
const bob: User = { id: "bob", isAdmin: false };
const carol: User = { id: "carol", isAdmin: true };
const handler = new CancelOrderHandler(orders);
const requests: ReadonlyArray<readonly [CancelOrder, User]> = [
[{ orderId: "order-1", reason: "changed mind" }, alice],
[{ orderId: "order-1", reason: "changed mind" }, alice],
[{ orderId: "order-2", reason: "too slow" }, bob],
[{ orderId: "order-2", reason: "customer call" }, carol],
[{ orderId: "order-9", reason: "typo" }, alice],
[{ orderId: "order-3", reason: " " }, alice],
[{ orderId: "order-3" }, alice],
];
for (const [command, actor] of requests) {
const result = handler.handle(command, actor);
if (!result.ok) {
// Only code and stage are available in this branch.
console.log(`${actor.id} cancels ${command.orderId}: Err ${result.code} at ${result.stage}`);
continue;
}
console.log(`${actor.id} cancels ${command.orderId}: Ok ${result.value}`);
}alice cancels order-1: Ok Cancelled
alice cancels order-1: Err AlreadyCancelled at Rule
bob cancels order-2: Err PermissionDenied at Authorize
carol cancels order-2: Err AlreadyShipped at Rule
alice cancels order-9: Err OrderNotFound at Load
alice cancels order-3: Err ReasonMissing at Validate
alice cancels order-3: Err ReasonMissing at ValidateThe output matches the C# example apart from its final bug line. Removing the order guard makes tsc --strict report 'order' is possibly 'undefined' (TS18048). Removing the reason guard reports an attempt to assign string | undefined to a string | null field (TS2322). Narrowing is a compile-time check; values arriving at runtime, such as JSON, still need validation.
See also
Each C# example is a standalone .NET 10 console program.
Compile the TypeScript examples to JavaScript targeting ES2022 or later, then run them in Node.js.
Footnotes
- Kent Beck. Smalltalk Best Practice Patterns (1996; copyright dated 1997). Publisher's page. Published October 3, 1996. Guard Clause entry in Chapter 7, Formatting. β©
- Kent Beck. Implementation Patterns (2007). Publisher's page. Chapter 7, Behavior: Guard Clause. The opening summary appears on page 1 of the InformIT excerpt. β©
- Kent Beck. Excerpt from Implementation Patterns, Guard Clause section. InformIT (2008). β©
- Martin Fowler. Replace Nested Conditional with Guard Clauses. Refactoring catalog. The catalog home page explains that it accompanies the second edition and also includes first-edition refactorings; the publication year comes from the book page. β©
- Kent Beck. Excerpt from Implementation Patterns, Guard Clause section. Paragraphs on the single-entry, single-exit rule. β©
- Microsoft Learn. Exception-handling statements. The try-finally section. β©
- Microsoft Learn, Framework Design Guidelines. Using Standard Exception Types, Exception Throwing. These pages reproduce material from the second edition (2008). β©
- Microsoft Learn API reference. ArgumentNullException.ThrowIfNull, ArgumentException.ThrowIfNullOrEmpty, ArgumentOutOfRangeException.ThrowIfNegative, CallerArgumentExpressionAttribute. Versions follow each page's Applies to list. β©
- Microsoft Learn, Framework Design Guidelines. Exception Throwing (the entries saying not to return error codes and to avoid exceptions for normal control flow where possible); Exceptions and Performance (Tester-Doer and Try-Parse). β©
- TypeScript Handbook. Narrowing. Control flow analysis section. β©
- Kent Beck. Excerpt from Implementation Patterns, Guard Clause section. Paragraph on continue in loops. β©