# Some functions can both fail and return nothing

In the [previous post](https://dimagog.hashnode.dev/better-error-handling-with-just-two-union-types) we've limited Union Types to just two kinds: `?T` for `T | None` and `!T` for `T | Error`. One of the corollaries was that `?!T`/`!?T` is not supported, because it would mean `T | None | Error`, and we strive for simplicity.

But a lookup that can both fail *and* find nothing is common enough:

```go
// the DB may be down (Error), or the key may be absent (None)
func Find(key: string) !?Record
```

Let's see what it takes to support `!?T` without giving up on simplicity.

## The obvious options

1. **Treat "not found" as an `Error`** and return `!T`. This is what Go does with `sql.ErrNoRows` and `fs.ErrNotExist`. "No value" becomes a special error, and callers have to compare error values to tell the two apart. But "no value" is an expected outcome, not an error, and the type signature should indicate this. It also means `try` propagates "not found" up the stack as if something went wrong.
2. **Keep the layers**, as Zig does: `!?T` is an error wrapping an optional, unwrapped one layer at a time. It works, but every caller has to unwrap twice, and `?!T` ends up being a different type from `!?T`, which violates the "Union Types are sets" approach.

### Tempting option 3: Derive `None` from `Error`

If `None` is a subtype of `Error`, then `?T` is implicitly convertible to `!T`, and `T | None | Error` is the same as `T | Error`. In other words, `!?T` is just `!T`. We get a simple chain `T <: ?T <: !T`, and the nesting problem goes away without any new types.

There are many benefits to this approach:

- `Find` simply returns `!T`, and "not found" is `none`, which is now a valid `Error` value. The caller checks `if r := Find(key) {} else if r is None {} else { log(r) }`.
- The boolean context rule becomes simpler: a value is falsy if it is `false` or `Error`, and truthy otherwise.
- Changing a return type from `!T` to `?T` is now a narrowing, so it passes the Litmus Test from the previous post.
- `try` needs no extra rules: `try` on `?T` works inside functions returning `?U` or `!U`, and `try` on `!T` only inside functions returning `!U`.

But there is one problem, and it is a big one: `none` becomes an error without any context. `var f: !File = none` compiles, and `log(f)` has nothing useful to print. Worse, `try m.Get(key)` inside a function returning `!Config` propagates `none` to the caller instead of saying which key was missing, and nothing in the type system reminds you to do better.

This is not a theoretical concern. Rust tried exactly this: `std::option::NoneError` on nightly allowed `?` to convert an `Option` into a `Result`, and it was removed for this very reason. Zig requires an explicit conversion instead: `orelse error.NotFound`. And Go's `io.EOF` is the same idea in practice: an "error" that is really an expected absence, with `if err == io.EOF` checks everywhere.

More fundamentally, half of the argument in the Go post was that the absence of a result is not an error. Making `None` an `Error` gives that up for `!T`. We want `None` and `Error` to remain distinct types, so the compiler knows which one we meant.

## Better option: Sealed Common Base

Instead, let's derive both `None` and `Error` from a common base type `Failure`:

```go
sealed type Failure = None | Error
```

`Failure` is sealed, i.e., `None` and `Error` are its only subtypes. Now the Union Type algebra does the rest:

- `??T` is the same as `?T`, because `T | None | None` is `T | None`
- `!!T` is the same as `!T`, because `T | Error | Error` is `T | Error`
- `!?T` is the same as `?!T`, because both are `T | None | Error`, which is `T | Failure`

Nesting flattens, the order does not matter, and `!?T` is the only new type. We still have just two *primitive* kinds of Union Types, and nesting them produces the third one. Let's use `!?T` as the canonical spelling.

### Conversions

Every type is implicitly convertible to the types below it, but not the other way around:

```text
Error  T  None
   | /   \ |
   !T     ?T
     \   /
      !?T
```

In code:

```go
var rec : Record
var r : !?Record = rec        // T to !?T
    r = none                  // None to !?T
    r = Err("DB is down")     // Error to !?T
var m : ?Record = rec
    r = m                     // ?T to !?T
var e : !Record = rec
    r = e                     // !T to !?T

e = m                         // rejected: ?T is not convertible to !T
m = e                         // rejected: !T is not convertible to ?T
```

The last two lines are the whole point of not going with option 3: `None` and `Error` remain distinct, and the compiler will enforce it.

## Boolean context and narrowing

The rule from the previous post actually becomes simpler: a value is falsy if it is `false` or a `Failure`, and truthy otherwise.

Narrowing works the same way: in the `else` branch `!?T` is narrowed to `Failure`, and one more type check tells `None` and `Error` apart:

```go
if r := Find("bob") {
    // r is Record here
    r.Greet()
} else if r is None {
    // r is None here
    CreateUser("bob")
} else {
    // r is Error here
    log(r)
}
```

For comparison, Zig needs both `catch` and `orelse` for the same thing.

## `try` and `!?T`

The `try` prefix from the previous post needs no new rules. It returns the failure to the caller, so the failure must be convertible to the return type of the enclosing function:

| `try` applied to | inside `?U` func | inside `!U` func | inside `!?U` func |
| --- | --- | --- | --- |
| `?T` | ✅ | ❌ | ✅ |
| `!T` | ❌ | ✅ | ✅ |
| `!?T` | ❌ | ❌ | ✅ |

Converting between kinds has to be explicit:

```go
func LoadUser(key: string) !User {
    r := try Find(key)            // rejected: Find may return None, LoadUser cannot
    r := try Find(key) ?? Err("user not found: " + key) // OK: None replaced by an Error
    return User(r)
}
```

With option 3 the first line would have compiled, and the caller would have received an error saying nothing.

## Litmus Test

Let's run the same test as in the previous post: which signature changes do not break the callers?

### Extending Inputs

Changing a parameter type from `?int` to `!?int` is a widening, so old callers passing an `int` or `none` still work. Whether a function should ever accept an `Error` as input is a separate question, but the type system does not need to forbid it.

**Test passed!**

### Restricting Outputs

`!?T` can be narrowed in two directions: to `!T` or to `?T`, and then further to `T`. Both paths are backward compatible, same as before.

**Case 1: `!?T` -> `!T`, no more `none`.** Suppose we've learned how to never return `none` from `Find`:

```go
// v1
func Find(key: string) !?Record
// v2: never returns none
func Find(key: string) !Record

// old callers still work, even though the none branch is dead now
if r := Find("bob") {
    r.Greet()
} else if r is None {
    CreateUser("bob")
} else {
    log(r)
}
```

**Test passed!** Note that in the dead `else if` branch `r` is of type `Error`, so `r is None` is always `false`. Perhaps the compiler generates an "always false" warning, similar to the "always true" warning in the previous post.

**Case 2: `!?T` -> `?T`, no more errors.** Now suppose instead that `Find` never has to return errors anymore:

```go
// v1
func Find(key: string) !?Record
// v2: never returns an Error
func Find(key: string) ?Record

// old callers still work, even though the error branch is dead now
if r := Find("bob") {
    r.Greet()
} else if r is None {
    CreateUser("bob")
} else {
    log(r) // dead: r is Failure but not None, and Error is impossible
}
```

**Test passed!** Here the compiler is less likely to notice the dead `else` branch, since `else` has no condition to evaluate. It also does not change anything for `try`: `try Find(key)` inside a function returning `!U` was rejected before (`Find` could return `none`) and is still rejected after.

Changing `!T` to `?T` or back is neither a narrowing nor a widening, so it is a breaking change. And it should be: it changes the meaning of the function, and the compiler should say so.

## What does it cost?

- **A third type.** It is derived rather than declared, but it exists, and `!?T` will show up in function signatures.
- **Two-step narrowing.** Telling `None` from `Error` takes a second `if`. Zig has the same cost.

## Conclusion

In [Why Go-style error handling is not great](https://dimagog.hashnode.dev/why-go-style-error-handling-is-not-great) we've called a function **partial** if it is unable to produce a return value for all possible inputs. `T | Failure` is exactly that: either a value, or the reason there is none. `?T` and `!T` cover the two possible reasons, and `!?T` covers both.

Two primitive Union Types and one sealed base type are all it takes to support nesting.
