13. Exceptions
DirectSharp preserves ordinary .NET propagation, handler selection, filters, rethrow and cleanup. Retry restarts protected work explicitly; best-effort forms deliberately discard an exception as their operational result.
13.1 Structured handlers
CS-EXCEPTION-001. try ... catch ... finally ... end try protects its body. A catch group is written entirely before the body or entirely after it; the forms cannot be mixed. Handlers-first is declarative and is not executed on normal entry. Finally is last, before end try.
CS-EXCEPTION-002. Typed catches use catch Type, optionally a comma-separated group of types. An implicit ex denotes the candidate exception in its filter and handler, with a safe statically known exception type. catch name as Type supplies an explicit name. when condition is an exception filter with ordinary .NET selection semantics. Bare catch is the catch-all and must be the last logical handler.
action SubmitWithRetry(gateway as PaymentGateway, request as PaymentRequest)
returns PaymentReceipt
try
return gateway.Submit(request)
catch Global.System.TimeoutException
if LastAttempt.Number < 3 then
retry
end if
throw
catch error as Global.System.IO.IOException when error.Message is not empty
Global.System.Console.WriteLine(error.Message)
throw
finally
gateway.RecordAttemptFinished()
end try
end action
The gateway is an ordinary application dependency with Submit and RecordAttemptFinished actions; PaymentReceipt is its result contract. The filter decides whether the IOException handler applies. A false filter permits later handlers or propagation. Failure in the handler is not treated as another execution of the protected body.
CS-EXCEPTION-003. Bare throw is a rethrow in an active catch, retaining the original exception propagation information. A typed exception expression can be thrown explicitly. A rethrow outside a valid catch is rejected. Handler ordering, unreachable handlers, catch-variable visibility and member accesses must be statically checked; exception class names are normal .NET names, with no removal of their Exception suffix.
13.2 Retry and cleanup
CS-EXCEPTION-004. Retry is a structured-try control transfer restarting the current protected body at its first statement. Protected-body locals are recreated; changes to outer variables persist. It does not imply delay, logging, backoff, maximum attempts or cancellation.
CS-EXCEPTION-005. Retry does not execute the associated finally because it does not leave the structured try. Retry in finally is invalid. Finally runs when the try is actually left through normal completion, return, targeted exit or propagation. Cleanup required between attempts must therefore be explicit or have an inner resource scope whose lifetime ends before the retry.
The sample permits at most three failed attempts. An IOException path does not retry. The gateway's final observer runs once when this try is left, rather than between timeout attempts. If that final observer throws, ordinary finally exception replacement rules apply.
13.3 Last completed attempt
CS-EXCEPTION-006. In an applicable lexical context, LastAttempt is present before the first try/try_get begins. Its initial record has Number 1, Exception nothing and HasErrors false. Reading that initial record does not require a previously completed attempt or raise an unavailable-state failure. These initial fields do not assert that protected work has already succeeded.
Every executed structured try, blockless try and inline/multiline try_get updates LastAttempt for its containing lexical block. After completion, it describes the most recently completed attempt and remains observable after that construct in the same block. A nested block's try/try_get updates that nested block's state without overwriting the enclosing block's state. Each such context has its own initialized record. LastAttempt is lexical observation metadata, not process-wide or thread-wide exception history.
| Member | Meaning |
|---|---|
| Number | Initially 1; thereafter the one-based number of the completed attempt |
| Exception | Its exception, or nothing after successful completion |
| HasErrors | True exactly when Exception is not nothing |
A catch observes the failed attempt it is handling. While a retry is executing, metadata continues to describe the prior completed attempt until the new attempt completes. A later successful attempt clears Exception and HasErrors. There is no ambient exception-history collection or LastAttempt.Result; application histories belong in ordinary variables.
Initialization supplies the context's first record; it does not reset an existing completed observation merely because another try starts. Reads before the first completion see the initial record, and subsequent in-progress attempts retain the latest completed observation until they complete.
For simple forms, the completed attempt's Number is 1. A successful computation returning nothing still records success: Exception is nothing and HasErrors is false. A catchable failure converted to nothing records that exception and HasErrors true. A later completed try/try_get at the same lexical level replaces the observation; copy needed details into ordinary locals before another operation if they must be retained. Reading LastAttempt without an applicable lexical context is a compile-time error.
let port = try_get Integer.Parse(text)
if LastAttempt.HasErrors then
Global.System.Console.WriteLine(LastAttempt.Exception.Message)
end if
let effectivePort = port ?? 443
The fragment observes the parse attempt before another try/try_get can replace its metadata. The result alone cannot distinguish a successful nothing from a failed acquisition; LastAttempt can.
13.4 Best-effort value acquisition and statements
CS-EXCEPTION-007. try_get expression yields the expression's value on success and nothing on any normally catchable exception. It provides no embedded handler, exception variable, logging, retry or cleanup clause. A failure from an explicit await or a protected structured operation's generated waits is part of the protected computation, not merely a failure during task creation. Capturing a task without awaiting it protects invocation only; a later operation failure is not retroactively suppressed.
let amount = try_get Decimal.Parse(text)
let port = try_get LoadPort() ?? 443
A successful stored nothing and an acquisition failure are intentionally indistinguishable through the result alone. A value-type result must support absence; it cannot turn failure into zero or another unrequested default.
CS-EXCEPTION-008. Multiline try_get protects its statements and one final take expression. Exactly one final take is required in a value context. Multiple/path-local/nonfinal take statements and attached catches/finally/retry clauses are invalid.
let customer = try_get
let response = api.Load(id)
ValidateResponse(response)
take response.Customer
end try_get
The API/validation names are ordinary application members. Failure anywhere through the take expression yields nothing. A surrounding or nested using scope may provide ordinary cleanup; try_get itself does not acquire ownership.
CS-EXCEPTION-009. Blockless try statement executes a statement best-effort and continues after any normally catchable exception. It has no value. A call's ignored result remains ignored. Use try_get when a value is required and structured try when failures need classification or explicit handling.
try cache.Refresh()
The statement does not automatically log or introduce an exception variable. Its completed result is observable through LastAttempt in the containing block. Use structured try for typed handling, filters, retry or cleanup. These constructs do not introduce a production no_throw clause or a second success/failure-handler language.
13.5 Reference lowering and diagnostics
A simple lookup has the conceptual lowering:
Customer? customer;
(int Number, Exception? Exception, bool HasErrors) lastAttempt = (1, null, false);
try
{
customer = repository.Get(id);
lastAttempt = (1, null, false);
}
catch (Exception error)
{
customer = null;
lastAttempt = (1, error, true);
}
The example illustrates value-or-absence acquisition plus the containing block's completed-attempt metadata, without inventing an exported runtime helper API. A later try/try_get in that block writes the same observation storage; a nested block uses separate storage. Retry requires control flow around the protected body inside an outer cleanup lifetime; translating it to continue across an ordinary C# finally would wrongly run that finally for every retry.
Reject split catch groups, finally before protected work, catch-all before later logical handlers, ill-typed filters, inaccessible catch locals, invalid rethrow, retry without an applicable structured try, retry in finally, handler clauses on try_get, blockless try in a value context and multiline try_get without its final value.
Specification contents · Previous: 12. Contracts · Next: 14. Receiver and resource scopes