# x! — the null-forgiving operator

> A postfix `!` tells the compiler a value it types as possibly-null is present — C#'s null-forgiving operator. It is a compile-time promise only: it changes no type and no behaviour, so when the promise is wrong a member read off the null throws `NullReferenceException`, as in C#.

<!-- id: types-null-forgiving · area: types · stability: stable · html: https://osysharp.com/reference/types/null-forgiving/ -->

## Summary        {#summary}
`x!` says "I know this is here" about a value the compiler types as possibly absent — a `Keeper?` returned by a
function, a `FirstOrDefault()`, a nullable reference. It is C#'s null-forgiving operator with C#'s meaning: a
**compile-time promise**, answered by the two places the compiler asks about absence, and nothing else. It changes
no type, no stored value and no runtime behaviour — if the promise is wrong, the value is still null, and reading a
member off it throws [`NullReferenceException`](#null-read), exactly as it would without the `!`.

## Signature      {#signature}
```osy syntax
value!                     // the value, promised present
Find(id)!.Name             // chains like any postfix: calls, member reads, indexers
names[0]!                  // after an indexer
a!.Next!.Name              // at every hop that needs it
x! != null                 // `!=` is still inequality — the lexer reads it as one token
```

## Description    {#description}

### When do I reach for it?   {#purpose}
When **you** know a value is present and the compiler cannot see why. A lookup by a key you just wrote, a row an
earlier step guarantees, a navigation whose target is `[Required]` in practice though the model allows none: the
value is typed nullable, the program is right, and the compiler has no way to prove it.

Two places ask about absence, and `!` answers both:

| where | without `!` | with `!` |
|---|---|---|
| a nullable value into a non-nullable local — `Keeper k = Find(id);` | refused: *cannot initialize non-nullable 'Keeper k'* | accepted |
| a member read off a nullable in a function — `Find(id).Name` | the SHOULD lint `correctness-member-read-off-a-nullable` (C#'s CS8602): it throws at run time if the value is null | quiet — and it still throws if the promise is wrong |

```osy title="a lookup the caller guarantees" test app=types-null-forgiving
entity Lighthouse {
  [MaxLength(60)] string Name;
  int Height;
  Lighthouse? Relief;                              // the station that takes over when this one is dark
  security { allow read, create when IsAuthenticated || IsAnonymous; }
}

Lighthouse? Find(string name) {
  return Lighthouse.FirstOrDefault(l => l.Name == name);
}

string ReliefFor(string name) {
  Lighthouse station = Find(name)!;                // refused without the `!`
  return station.Relief!.Name;                     // and each hop that can be absent says so where it is read
}
```

### A `var` carries the promise   {#var}
`var keeper = Find(id)!;` makes `keeper` present for everything after it, the way C#'s flow state does: it can be
handed to a non-nullable local, and its members read without the lint.

```osy title="the promise travels with the local" test app=types-null-forgiving
string NameOf(string name) {
  var station = Find(name)!;
  Lighthouse again = station;                      // no refusal — `station` was promised present
  return again.Name;
}
```

### It is a promise, not a check   {#not-a-check}
`!` does **not** test the value and does not itself throw. When the promise is wrong, the null arrives exactly as it
would have without the `!` — a comparison with `null` is still true, a query that compares against it still matches
a null, and a member read off it does what a member read off null does: it throws. That is C#'s behaviour too, and
it is deliberate: the operator is for telling the compiler something, never for changing what runs.

### A member read off null throws   {#null-read}
In a function body — a function, a class method, a page's `action`, a workflow body, a test — reading a member off
null with a plain `.` throws `NullReferenceException`, as C# does. The message names the member and what was null:

```text
`station.Relief` is null, so `.Name` cannot be read off it. Check it for null first, or read with `?.`.
`name` is null, so `.Trim(…)` cannot be called on it. Check it for null first, or call with `?.`.
```

It is the same exception and the same sentence on the server and in the browser, and a typed `catch` takes it.

```osy title="thrown, caught by type, and avoided" test app=types-null-forgiving
string ReliefOrNone(string name) {
  var station = Find(name);
  try { return station!.Relief!.Name; }                 // throws when there is no such station, or no relief
  catch (NullReferenceException) { return "none"; }
}

string ReliefIfAny(string name) {
  return Find(name)?.Relief?.Name ?? "none";            // `?.` answers null instead, and skips the rest of the chain
}
```

| what is read off null | what happens |
|---|---|
| a field, a property, a reference to another row — `station.Name`, `station.Relief.Name` | throws |
| a method — `name.Trim()`, `name.Length`, `station.Describe()` | throws |
| an assignment — `station.Name = "x"` | throws |
| a list — `list.Count`, `list[0]`, `list.Add(x)` | throws |
| with `?.` — `station?.Name`, `station?.Relief.Name`, `list?.Count` | null; the rest of the chain is not read |
| a function of the language called by name — `Text.Trim(name)`, `string.IsNullOrEmpty(name)` | takes the null as an argument; no throw |

Two positions keep a rule of their own, because the code in them is not a function body:

- **A slot of a page** — `Text(order.Customer.Name)` in a `render` — draws nothing for a null. One row with an empty
  reference must not take the whole page down. The same read in the page's `action` throws.
- **A query clause over stored rows** — `Order.Where(o => o.Customer.Name != "x")` — is the database's: an empty
  reference is null there, `!=` keeps the row, and nothing throws. (A lambda over a list you already hold —
  `boxes.Select(b => b.Label)` — is a function body, and throws.)

Three reads C# also throws for still answer without throwing: `.Value` on a nullable number or date that is null
(it answers null), `foreach` over a null list (no iteration), and a LINQ call on a null list (an empty result).

When the value genuinely might be absent, say what absence means instead:

| you mean | write |
|---|---|
| absent is a case I handle | declare it nullable — `Lighthouse? station = Find(name);` — and test it: `if (station == null) { … return; }` |
| absent answers absent | `Find(name)?.Name` |
| absent is a bug that should fail loudly, here | `First(…)` rather than `FirstOrDefault(…)` — it throws when nothing matches |

### It does not unwrap a number, a date or an enum   {#value-types}
`!` forgives a possibly-null **reference** — an entity, a `string`. It does not turn an `int?` into an `int`, a
`DateTime?` into a `DateTime`, or an `enum?` into its enum: C# refuses `int n = maybe!;` (CS0266) and so does
Osy#, and the refusal says why. The unwrap there is `.Value` (which throws when absent) or `??` (which supplies a
value):

```osy title="a value type is unwrapped, not forgiven" test app=types-null-forgiving
int Highest() {
  return Lighthouse.Max(l => l.Height) ?? 0;   // `Max` answers null over no rows; `??` says what that means
}
```

```text
cannot initialize non-nullable 'int n' with the result of a `Max(…)` aggregate, … (`!` does not unwrap `int?`
— it forgives a possibly-null REFERENCE, and C# refuses this too, as CS0266. `.Value` unwraps, throwing when
absent; `??` supplies a value.)
```

`Lighthouse b = null!;` is refused as well. C# uses `null!` for a **field** that a constructor fills in later; an
Osy# local has nothing that could, so the promise could only ever be broken.

## Examples       {#examples}
```osy title="every position" test app=types-null-forgiving
string Direct(string name) { return Find(name)!.Name; }                 // off a call
bool Known(string name) { var s = Find(name); return s! != null; }       // `!=` is still inequality
string Indexed(string name) {
  var names = new List<string?> { name };
  return names[0]!;                                                      // after an indexer
}
string Chain(string name) { return Find(name)!.Relief!.Name; }          // at every hop
```

## See also       {#see-also}
- [Optional and required members](https://osysharp.com/reference/types/optional-and-required/) — how a type is spelled nullable in the first place
- [entity members](https://osysharp.com/reference/entity/properties/) — `[Required]` references, whose absence the model rules out rather than you promising it
- [Sum / Average / Min / Max / Count](https://osysharp.com/reference/query/aggregates/) — why `Max`/`Min`/`Average` answer null over no rows, and `??` for what that means
