# try / catch / finally

> Handles a fault. C#'s syntax, including typed catches, catch filters and finally. The rows written inside a try block that throws are discarded — so a caught fault leaves your data where it was, not half-changed.

<!-- id: function-try-catch · area: function · stability: stable · html: https://osysharp.com/reference/function/try-catch/ -->

## Summary        {#summary}
`try` / `catch` / `finally` is C#'s, and it behaves the way you expect — with one addition that is the reason to reach
for it here: **a `try` block that throws discards the rows it wrote.**

So a caught fault does not leave you holding half-applied changes to clean up. The block either happened or it did
not:

```osy title="the rejected row does not survive the catch" test app=function-try-catch
entity Order {
  [Required, Unique, MaxLength(20)] string Code;
  decimal Total;
  invariant Total >= 0;                       // broken → a catchable ValidationException

  // No `security { }` block means denied to everyone, so every example declares its grants.
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

entity AuditLine {
  [Required, MaxLength(200)] string Message;
  security { allow create, read when IsAuthenticated || IsAnonymous; }
}

string Place(string code, decimal total) {
  try {
    var order = new Order { Code = code, Total = total };
  }
  catch (ValidationException e) {
    var line = new AuditLine { Message = $"rejected {code}: {e.Message}" };
    return "rejected";                // the bad Order is gone; this AuditLine is kept
  }
  return "placed";
}
```

## Signature      {#signature}
```osy syntax
try {
  …
}
catch (<ExceptionType> e) when (<filter>) {   // the type, the name and the `when` are each optional
  …
}
catch {                                       // a bare catch-all
  …
}
finally {                                     // always runs — including on return, break and continue
  …
}
```
A `try` must be followed by a `catch` or a `finally` (or both). A lone `try` is a compile error.

## Description    {#description}

### What you can catch   {#what-throws}
Three different things raise a fault, and only the first is yours:

| What happened | You catch it as |
|---|---|
| **You threw** ([throw](https://osysharp.com/reference/function/throw/)) | the type you threw — or `Exception` |
| **A [[entity-constraints\|constraint]] or [[entity-invariants\|invariant]] was broken** | **`ValidationException`** |
| **A concurrent write collided with yours** | **`ConflictException`** |
| A division by zero, a member read on a null | **only `Exception`** — these have no specific type |

The second row is the one that changes how you write code. A broken invariant is not a database error you have to
recognise by its text — the platform raises it as an ordinary `ValidationException`, in the same closed vocabulary as
the ones you throw yourself. Your validation rules live on the entity, where they are enforced for **every** writer,
and a caller that wants to *handle* a violation rather than fail on it just catches it.

### The rollback is per-block, not per-function   {#rollback}
A function is transactional — a fault it does not catch discards everything it wrote ([function](https://osysharp.com/reference/function/declaration/)). A
`try` block is the same idea, **scoped to the block**:

- The rows written inside the `try` are discarded if it throws.
- The rows written inside the `catch` are kept — the handler is not tarred with the failure it is handling.
- Everything the function wrote **before** the `try` is untouched.

That is what makes the pattern above honest: the rejected `Order` cannot linger, and the `AuditLine` recording the
rejection is still there to read. Proved, not asserted:

```osy title="what survives a caught fault, and what does not" run app=function-try-catch
[Test]
void A_broken_invariant_arrives_as_a_ValidationException() {
  Assert.Equal("rejected", Place("A1", -5m));       // the invariant `Total >= 0` was broken

  Assert.Empty(Order.ToList());                     // the try block's row did NOT survive
  Assert.Single(AuditLine.ToList());                // …and the catch block's row DID
}

[Test]
void An_acceptable_row_is_simply_written() {
  Assert.Equal("placed", Place("A2", 10m));

  Assert.Single(Order.ToList());
  Assert.Empty(AuditLine.ToList());
}
```

### Catching by type, in order   {#by-type}
Clauses are tried **in source order**, and the first one whose type matches wins. `Exception` is the base of all of
them, so a `catch (Exception e)` matches anything — which makes it the last clause you write, never the first:

```osy title="the specific case, then the general one" test app=function-try-catch
string Describe(string code, decimal total) {
  try {
    var order = new Order { Code = code, Total = total };
    return "placed";
  }
  catch (ValidationException e) { return $"invalid: {e.Message}"; }   // the row broke a rule
  catch (ConflictException e)   { return $"conflict: {e.Message}"; }  // someone else got there first
  catch (Exception e)           { return $"failed: {e.Message}"; }    // anything else at all
}
```

The caught value carries a **`Message`** — the text of the throw, or the platform's explanation of the rule you broke
— and a **`Type`**.

### `when` filters a catch   {#when}
A `when` clause decides whether *this* handler is the right one, without catching and rethrowing to find out. The
caught value is in scope inside the filter, so you can look at the message before committing to handle it:

```osy title="handle only the case you know how to handle" test app=function-try-catch
string Refund(string code, decimal amount) {
  try {
    var order = Order.Single(o => o.Code == code);
    if (amount <= 0m) { throw new ValidationException("refund must be a positive amount"); }

    order.Total = order.Total - amount;   // may leave Total negative → breaks the invariant
    return "refunded";
  }
  catch (ValidationException e) when (e.Message.StartsWith("refund")) {
    return "declined";       // MY refusal — the one case this function knows how to answer for
  }
  // the broken invariant is ALSO a ValidationException — but its message is not mine, the filter
  // does not match it, and it keeps travelling out to the caller
}
```

Both faults here are `ValidationException`. The filter is what tells them apart — so filter on a message **you
threw**. The platform writes its own text for a rule it enforces on your behalf, and that text is its to change;
matching on it would couple your control flow to wording you do not own.

An unmatched fault is not swallowed: it travels up to the caller, and if nobody catches it the function faults and
discards its writes.

```osy title="same type, different message — the filter decides" run app=function-try-catch
[Test]
void The_filtered_catch_handles_the_case_it_recognises() {
  Place("A9", 100m);

  Assert.Equal("declined", Refund("A9", -5m));                    // my message → filtered in, handled
  Assert.Equal(100m, Order.Single(o => o.Code == "A9").Total);    // …and the declined refund changed nothing
}

[Test]
void A_fault_the_filter_does_not_match_keeps_travelling() {
  Place("A9", 100m);

  // Refunding 500 from 100 leaves Total at -400, which breaks `invariant Total >= 0`. Same exception TYPE,
  // but not my message — so the `when` skips it, Refund does NOT return "declined", and the fault leaves.
  Assert.Throws<ValidationException>(() => Refund("A9", 500m));

  Assert.Equal(100m, Order.Single(o => o.Code == "A9").Total);    // the try block's write went with the fault
}
```

### `finally` always runs — but it cannot outlive a fault   {#finally}
`finally` runs on the way out however you leave: off the end, on a `return`, and on a `break` or `continue` that
leaves a loop from inside it.

```osy title="finally runs on the way out, however you leave" test app=function-try-catch
string Attempt(string code) {
  try {
    if (code == "") { throw new ValidationException("a code is required"); }
    return "ok";                                             // …the finally still runs on this path
  }
  finally {
    var line = new AuditLine { Message = $"attempted '{code}'" };
  }
}
```

**Now the part that catches people, and it follows from the transaction rather than from `finally`.** If the fault
escapes the function, the function's whole transaction is discarded — and the `finally` block is *part of the
function*, so the rows **it** wrote are discarded too. The block runs; its writes do not survive:

```osy title="what a finally can and cannot leave behind" run app=function-try-catch
[Test]
void On_the_normal_path_the_finally_write_is_kept() {
  Assert.Equal("ok", Attempt("A3"));

  Assert.Single(AuditLine.ToList());   // the function returned, so its transaction committed
}

[Test]
void When_the_fault_escapes_even_the_finally_write_is_discarded() {
  Assert.Throws<ValidationException>(() => Attempt(""));

  Assert.Empty(AuditLine.ToList());    // NOT one row: the fault took the whole function's writes with it
}
```

So **`finally` is not where you record that something failed.** A row written there survives only when the function
goes on to succeed — exactly the case where there was nothing to record.

To persist a record of a failure, **catch it**: a caught fault means the function completes normally, so the `catch`
block's writes commit (which is what the `Place` example at the top of this page relies on). If it must be recorded
even when the fault escapes, use [`Log.Error`](https://osysharp.com/reference/diagnostics/log/) — a log line is not a row, and a rollback cannot take
it back.

### The one thing you cannot catch   {#uncatchable}
A **runaway function** — one that recurses without end, or loops without end — is stopped by the platform, and that
stop is **not catchable**. A `while (true) { try { … } catch { } }` cannot swallow its own kill signal and keep going.

This is deliberate: the limit exists to protect everything else running on the platform, so it cannot be something an
app can opt out of by wrapping it in a handler.

### The caveat: `UnitOfWork.Commit()` is a real write   {#commit-caveat}
Everything above is about rows that have not been persisted yet — the ordinary case, because a server function
persists **when it returns** ([function](https://osysharp.com/reference/function/declaration/)).

If you call `UnitOfWork.Commit()` explicitly in the middle of a function, that is a real write. **A fault afterwards does not
un-write it.** The rollback covers what has accumulated *since* the commit, not what the commit already made durable.
An explicit mid-function `UnitOfWork.Commit()` is therefore a decision to give up all-or-nothing for what came before it — which
is occasionally what you want, and never what you want by accident.

## See also       {#see-also}
- [throw](https://osysharp.com/reference/function/throw/) — raising one, and the closed set of types
- [function](https://osysharp.com/reference/function/declaration/) — the function is the transaction; the `try` block is a smaller one inside it
- [invariant](https://osysharp.com/reference/entity/invariants/) · [constraints](https://osysharp.com/reference/entity/constraints/) — the rules whose violation arrives as `ValidationException`
- [Assert](https://osysharp.com/reference/testing/assert/) — `Assert.Throws<T>`, for proving a function refuses what it should
