Summary#
throw raises a fault. The function stops where it stands, and the rows it wrote are discarded — a function that
throws leaves nothing half-written behind it.
You throw one of a closed set of types. There is no class MyException to declare:
entity Order {
[Required, Unique, MaxLength(20)] string Code;
decimal Total;
// Every entity states who may touch it — with no `security { }` block it is denied to everyone.
// A real app scopes these grants to a user; an example still has to declare them, because an example
// that could not actually run is not an example. See the security guide.
security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}
void Refund(string code, decimal amount) {
var order = Order.SingleOrDefault(o => o.Code == code);
if (order == null) { throw new NotFoundException($"no order '{code}'"); }
if (amount > order.Total) { throw new ValidationException("a refund cannot exceed the order total"); }
order.Total = order.Total - amount;
}Signature#
throw new Exception("<message>"); // the base type — the catch-all
throw new NotFoundException("<message>"); // asked for a thing that is not there
throw new ValidationException("<message>"); // the input or the resulting row is not acceptable
throw new ConflictException("<message>"); // it clashes with the current state of the data
throw new OAuthConnectionFailedException("<msg>"); // an external authorization handshake failedDescription#
The five types, and nothing else#
The vocabulary is closed. Naming an unknown type is a compile error that lists the ones you may use, so you cannot misspell your way into a silent catch-all:
| Type | What it means | Also raised for you by |
|---|---|---|
Exception | the base. Every other type is one, so catch (Exception e) catches everything | — |
NotFoundException | you asked for something that does not exist | — |
ValidationException | the input, or the row you are about to write, is not acceptable | a broken [[entity-constraints|constraint]] or [[entity-invariants|invariant]] |
ConflictException | it cannot be done given the current state of the data | two writers colliding on the same rows |
OAuthConnectionFailedException | an external authorization handshake failed | the OAuth surface |
The point of the closed set is that a catch has a finite, knowable set of things to match on, and the two types
the platform raises on your behalf are in it. That is what lets you catch a constraint violation as an ordinary
ValidationException (try / catch / finally) rather than by inspecting a database error.
Why you cannot declare your own: a fault's type is what a catch matches on, and its message is what a human
reads. A bespoke type would carry no more information than the message already does, and it would not survive leaving
the app — see below.
A throw discards what the function wrote#
This is the part worth internalising. A function is transactional (function), and a throw is the abort:
void PlaceTwo(string first, string second) {
var a = new Order { Code = first, Total = 10m };
if (second == "") { throw new ValidationException("the second code is required"); }
var b = new Order { Code = second, Total = 20m };
}If second is empty, neither order exists. The first new Order was already written in the ordinary sense — it is
simply never committed. You do not unwind it, and there is no state in which the caller can observe it.
Proved, not asserted:
[Test]
void A_throw_discards_the_rows_the_function_had_written() {
Assert.Throws<ValidationException>(() => PlaceTwo("A1", ""));
Assert.Empty(Order.ToList()); // NOT one row — the first `new Order` went with it
}
[Test]
void The_type_is_what_a_caller_matches_on() {
Assert.Throws<NotFoundException>(() => Refund("nope", 1m));
}throw ends a path#
A throw satisfies the compiler's "all paths return a value" rule, exactly as it does in C#. A guard clause that
throws needs no else:
decimal TotalOf(string code) {
var order = Order.SingleOrDefault(o => o.Code == code);
if (order == null) { throw new NotFoundException($"no order '{code}'"); }
return order.Total; // reachable only when the order exists — no `else`, no null check
}What a caller outside the app sees#
Inside the app, a throw is caught by type (try / catch / finally).
A function reached from outside the app — over its published REST surface, or as a tool call — is different: a
fault becomes a failure carrying your message, and the type does not survive the crossing. So write the
message for the person who will read it, and do not expect an external caller to branch on NotFoundException versus
ValidationException. Inside, the type is everything; at the edge, the message is.
See also#
- try / catch / finally — catching one, and what else can throw
- function — why a fault discards the writes: a function is one transaction
- constraints · invariant — the rules that raise
ValidationExceptionfor you - Assert —
Assert.Throws<T>, which is how you prove a function refuses what it should