Handling errors and protecting invariants
Different failures call for different interfaces. A user submitting a form needs a list of corrections. An action receiving an impossible argument should fail immediately. A network failure may justify a retry. Keeping these cases separate makes the calling code easier to reason about.
require / ensure: state entry and exit rules
public class InventoryItem
public readonly Code as String
public Quantity as Integer with private set = 0
public constructor(code as String, quantity as Integer)
require arguments.code is not nothing
require arguments.code is not whitespace
require arguments.quantity >= 0
self.Code = arguments.code.Trim()
self.Quantity = arguments.quantity
end constructor
public action Reserve(count as Integer)
require count > 0
require count <= Quantity otherwise "Insufficient stock"
ensure Quantity >= 0
ensure Quantity = old_value_of(Quantity) - count
Quantity -= count
end action
public action Restock(count as Integer)
require count > 0
ensure Quantity >= 0
checked
Quantity += count
end checked
end action
end class
require checks at the point where execution reaches it. A failure stops the action immediately. The compiler supplies the default exception and a message identifying the failed condition, so ordinary checks need no otherwise text. Use otherwise for a useful domain explanation, such as Insufficient stock, or an intentional exception type; it is not another branch of arbitrary application code.
ensure checks on successful completion, including early returns. It is not an immediate assertion at the declaration line and does not run when an exception propagates. old_value_of(Quantity) refers to its value at action entry, so the reservation postcondition states the intended state transition.
Snapshot sources must be available at entry, not locals declared later in the body. Captures happen before body statements, including require guards; a getter can therefore run or fail before a guard. Constructor timing, capture order and source restrictions are covered in the contract specification.
These contracts do not make Reserve thread-safe. Several callers mutating the same inventory item require explicit coordination or a transactional persistence strategy.
otherwise: customize a contract failure
A parameter's absence usually produces ArgumentNullException; an attributable parameter constraint produces an appropriate argument exception. A state requirement and a failed postcondition use InvalidOperationException. A custom message preserves the contextual category. An explicitly constructed exception selects its stated type:
public class AvailableStock
public static action Check(requested as Integer, available as Integer)
require requested > 0
require available >= 0
require requested <= available otherwise new Global.System.InvalidOperationException(
"The stock changed before this reservation could be applied"
)
end action
end class
Use a validation library when the desired result is several user-facing errors. require is fail-fast and does not accumulate failures. The application chapter uses FluentValidation for that separate job.
try / catch: handle a known failure
This file preview action can explain a missing file or denied access without hiding every possible programming error:
public class ImportPreview
public static action Read(path as String)
returns String
require path is not nothing
require path is not whitespace
try
let content = Global.System.IO.File.ReadAllText(path)
return "Read {{content.Length}} characters"
catch Global.System.IO.FileNotFoundException
return "The import file no longer exists"
catch Global.System.UnauthorizedAccessException
return "The application cannot read this file"
end try
end action
end class
The action handles two failures with meaningful display results. Other exceptions propagate. Catching everything and reporting "file missing" would turn unrelated problems into misleading feedback.
Keep exception type names recognizable. IOException is an ordinary .NET type, not a language-specific shorthand such as IO. A bare catch is a catch-all, and ex denotes the current exception when no explicit variable is supplied. Use a named typed catch when its details are relevant.
catch when: filter a handler
Handle a missing customer without hiding other HTTP failures. Inside an action with an injected client and a supplied address:
try
return await client.GetStringAsync(address)
catch error as Global.System.Net.Http.HttpRequestException when error.StatusCode = Global.System.Net.HttpStatusCode.NotFound
return nothing
end try
Only HTTP 404 selects this handler; HTTP 500, transport errors and cancellation propagate. Filters are synchronous, and the named exception is available in both the filter and body. The customer lookup adds cancellation forwarding; chapter 13 covers filter failures and handler ordering.
catch: handle several exception types
A comma-separated list shares one handling path. With System imported and input supplied:
try
return Decimal.Parse(input, Global.System.Globalization.CultureInfo.InvariantCulture)
catch FormatException, OverflowException
throw new ArgumentException("Enter an amount within the supported range", "input", ex)
end try
The listed types and their derived types match; implicit ex is a System.Exception. Other failures propagate. See the credit-limit example for surrounding validation.
The complete catch group can also precede protected work, closed by end catch, as in the customer note reader. Both placements use the same handler and cleanup rules.
using / finally: clean up owned resources
Start with using when this operation owns the resource. This complete declaration closes its file before control leaves, whether reading succeeds or fails:
public class FilePreview
public static action Read(path as String)
returns String
require path is not nothing
require path is not whitespace
using reader = new Global.System.IO.StreamReader(path)
return reader.ReadToEnd()
end using
end action
end class
Acquisition happens before the body. If it fails, no body runs and there is no acquired reader to clean up. If the body returns, cleanup finishes before that return reaches the caller. A cleanup failure propagates instead of publishing successful completion. Several owned resources are cleaned up in reverse acquisition order.
Using chooses synchronous or asynchronous cleanup from the resource's static contract and the action's execution context. In an already async body, IAsyncDisposable is preferred when available; async-only cleanup can require permitted ordinary action inference. A dual-interface resource alone does not make an otherwise synchronous action async. Fixed synchronous boundaries never acquire a hidden blocking bridge. The resource reference gives the complete selection table.
Manual try/finally expresses the underlying obligation when you need to manage it directly:
public class FileStatistics
public static action Length(path as String)
returns Long
require path is not nothing
require path is not whitespace
let stream = Global.System.IO.File.OpenRead(path)
try
return stream.Length
finally
stream.Dispose()
end try
end action
end class
Once opening succeeds, finally releases the stream on normal return or on failure in the body. If opening itself fails, there is no acquired stream to release. Cleanup exceptions follow normal .NET exception behavior; they are not automatically swallowed.
Ownership must be clear. Dispose a resource that this operation owns. Do not dispose a shared injected service merely because the action finished using it. Async-only resources require their asynchronous disposal contract in an appropriate async context.
try_get / ??: provide a fallback for optional work
try_get returns a value or nothing. The result alone loses the distinction between a successful absent value and a failed acquisition, but LastAttempt retains the latest completed attempt's exception details in the containing lexical block:
let port = try_get Integer.Parse(text)
if LastAttempt.HasErrors then
Global.System.Console.WriteLine(LastAttempt.Exception.Message)
end if
let effectivePort = port ?? 443
Every executed structured try, blockless try and inline/multiline try_get updates LastAttempt. A later try/try_get in the same block replaces it; nested blocks maintain their own state. Simple forms report Number 1. Successful completion clears Exception and HasErrors even if the produced value is nothing. Preserve needed details in ordinary locals before another attempt.
LastAttempt is already present in its applicable context before the first try begins, with Number 1, Exception nothing and HasErrors false. The initial record does not mean work has completed. Once an attempt completes, its observation remains until another completes; starting new work does not reset it.
Best-effort acquisition suits an optional operation where failures share one fallback meaning. It provides no automatic logging or handling policy and does not replace typed catches for financial operations, persistence or security-sensitive checks.
retry restarts the associated try attempt and remains explicit. Retry policies need bounds, appropriate delays and knowledge of whether an operation is safe to repeat. A failed HTTP response does not prove that the server performed no work; payment and message operations commonly need idempotency keys or reconciliation.
For application examples, see the retrying Customer lookup and optional import configuration. The lookup retries GET only for HTTP 503 below a caller-supplied ceiling and checks cancellation before each request. Retry leaves its finally pending until the try is actually left; inner resource scopes still clean up between attempts. The configuration example uses inline try_get with an outside fallback and multiline try_get around an owned reader.
See contract semantics, exceptions and resource ownership.
Practice: contracts and failure handling
Find an exception observed outside its handler, then explain why cleanup must finish before success.
Previous: Collections and text · Next: Async and concurrency