How it works
Some operations routinely reject input as part of their normal contract. A small result type can describe that outcome with a success flag, a useful value, and a reason for failure. This keeps expected rejection visible at the call site without using exception handling as the ordinary path through every invalid form submission or import record.
The result shape should make its rules clear. A success result has a meaningful value; a failure result has a meaningful reason. A simple record with all fields available still permits inconsistent combinations, so factory functions or a more constrained model can be useful as the application grows. Start with an explicit convention, then enforce stronger invariants when the domain needs them.
Walk through the example. The ReadQuantity function first tries to parse an invariant integer and then rejects negative quantities. Successful input produces an accepted result containing the quantity. Malformed text and a negative quantity produce different error codes. The caller branches on Success before consuming Value, so a failure's placeholder zero is never treated as an accepted observation.
Watch the boundary. A result type should not swallow unexpected programming defects. Catching every exception and returning invalid hides problems unrelated to the input contract. Keep ordinary validation results separate from unexpected failures. Also avoid checking only whether Value is zero, because zero may be a valid quantity and cannot serve as a reliable success indicator.
Practice independently. Extend the quantity reader with an upper limit and a distinct too-large error. Prepare one case for every rejection reason and both valid boundaries. Move result construction into named success and failure factory functions, then explain which inconsistent combinations the factories help avoid. Finally decide whether the caller needs a stable machine-readable error code, a human message, or both.
A working example
using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
QuantityResult ReadQuantity(string text)
{
if (!int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value))
return new(false, 0, "not-integer");
if (value < 0) return new(false, 0, "negative");
return new(true, value, "");
}
foreach (string text in new[] { "4", "bad", "-1", "0" })
{
var result = ReadQuantity(text);
Console.WriteLine(result.Success ? $"accepted:{result.Value}" : $"rejected:{result.Error}");
}
record QuantityResult(bool Success, int Value, string Error);
Expected output
accepted:4 rejected:not-integer rejected:negative accepted:0
Use the success discriminator before reading the value, and preserve distinct rejection reasons.