# Better error handling with just two Union Types

In previous posts I've covered my beef with error handling in some programming languages:

- [Why Go-style error handling is not great](https://dimagog.hashnode.dev/why-go-style-error-handling-is-not-great)
- [Why TypeScript and Crystal have the best Union Types](https://medium.com/@dmitrykakurin/why-crystal-and-typescript-have-the-best-union-types-1a8fbfba831)

But what can we do without going all the way to full-blown Union Types? What if we don't need to represent types like `int | string` and are willing to limit our needs to optional values and the possibility of functions failing?

Let's take our inspiration from [Zig](https://ziglang.org/), which already does almost exactly this, and use the same `?T` and `!T` notation. However, we won't follow Zig 1:1. Also note that Kotlin, Swift, Dart, and C# have the `?T` half written as `T?`.

It's worth spelling out what exactly makes it work, and what it takes to pass the API evolution Litmus Test at the end.

## 1: Limited Union Types

The basic idea is to introduce just two possible kinds of Union Types:

1. For a type `T`, `?T` means `T | None`.
2. For a type `T`, `!T` means `T | Error`. Here `Error` is assumed to be a single interface type, like Go's `error`, i.e., no typed errors.

Corollaries:

1. It follows from #1 above that `??T` and `?T` are the same type because `T | None | None` is `T | None`.
2. The desire to have just two Union Types precludes `?!T`/`!?T`: it would mean `T | None | Error`, and we strive for simplicity *(but wait till you read [the next post](https://dimagog.hashnode.dev/some-functions-can-both-fail-and-return-nothing))*.

**Very importantly**, the following pseudocode should be accepted by the compiler (this implicit conversion is what `Maybe`/`Result` wrappers lack, and what makes the Litmus Test below pass):

```go
var x : string
var maybe_x : ?string = x // implicit T to ?T conversion
    maybe_x = none        // implicit None to ?T conversion
var error_x : !string = x // implicit T to !T conversion
    error_x = Err("Nope") // implicit Error to !T conversion
```

And the following should still be rejected:

```go
x = maybe_x // ?T is not convertible to T
x = error_x // !T is not convertible to T
```

In other words, `T` is a proper subtype of `?T` and `!T`.

## 2: Extending boolean context

In a boolean context, e.g., inside an `if` condition:

1. `?T` is truthy if it is `T` and falsy if it is `None`
2. `!T` is truthy if it is `T` and falsy if it is `Error`

## 3: Control-Flow-Sensitive typing

TypeScript calls it **Type Narrowing**, Crystal — **Type Restriction**, and Scala — **Flow Typing**. But the idea is the same: *at compile time*, a variable's type becomes more precise depending on code flow.

In our case, a variable of type `?T` or `!T` can be narrowed to type `T`, and to `None`/`Error` in the else branch.

## Examples combining it all together

An iterator returning the next element:

```go
func Next() ?T

for val := Next() {
    // even though Next returns ?T, here val has the type T
}
```

Opening a file:

```go
func Open(name: string) !File

if f := Open("test") {
    // f is of type File here, not !File
    f.Read(...)
} else {
    // f is of type Error here
    log(f)
}
```

## Function signature evolution Litmus Test

This is the same test that `Maybe`/`Result` wrappers fail in [Why TypeScript and Crystal have the best Union Types](https://medium.com/@dmitrykakurin/why-crystal-and-typescript-have-the-best-union-types-1a8fbfba831).

Imagine `v1` of our library had a function:

```go
func f(x: int) ?string
```

Now we are going to publish `v2` of this lib. What can we do to this interface without breaking the callers?

### Extending Inputs

A function is backward compatible if it can accept a wider range of inputs than before. Suppose we've learned to do something useful in our function without `x`, and we'd like to make `x` optional. Can we change the type of `x` to `?int`?

```go
func f(x: ?int) ?string

// old callers still work
f(3)

// but now this is possible too
f(none)
```

**Test passed!**

### Restricting Outputs

A function is backward compatible if it returns a narrower range of outputs than before. Suppose we've learned a way to always return a meaningful `string` from `f`, and it does not have to be optional anymore, as we never return `none`. Can we change the return type to `string`?

```go
func f(x: int) string

// old callers still work, even though the else branch is dead now
// perhaps the compiler generates an "always true" warning
if s := f(3) {
    s.ToUpper()
} else {
    UseDefault()
}

// but new callers don't have to check anymore:
f(3).ToUpper()
```

**Test almost passed**: we need to define how values of regular types (like `string` here) are evaluated in a boolean context. A simple rule could be: everything is truthy, except bool `false`, `None`, and `Error`. However, there are wrinkles when narrowing functions returning `bool`. Going from returning `?bool` to `bool` changes the semantics for older callers: they would take the "no value" branch when `false` is returned. But that's an acceptable limitation — we just postulate that such an interface change is not backward compatible; don't do it.

## Error Handling Ergonomics

Two more things are needed in practice, and both are orthogonal to the type system.

### Error Propagation Syntactic Sugar

Since `Error` converts implicitly to any `!T`, passing an error up the stack already works:

```go
func Load(name: string) !Config {
    if f := Open(name) {
        return Parse(f)
    } else {
        return f // f is Error here
    }
}
```

But as Yogi Berra said, "It's déjà vu all over again". It's not that different from Go's `if err != nil { return err }`.

A `try` prefix (as in Zig) or a `?` postfix (as in Rust) that returns the `Error` to the caller, and otherwise narrows to `T`, removes the boilerplate.

With a `try` prefix:

```go
func Load(name: string) !Config {
    f := try Open(name) // f is File here; if Open returns an Error, Load returns it as-is right away
    return Parse(f)
}
```

or with a `?` postfix and no local variable:

```go
func Load(name: string) !Config {
    return Parse(Open(name)?)
}
```

Similarly, supplying a default value for `?T` can be done with a `??`/`orelse` operator or an `OrElse` method.

```go
func Get(i: int) ?int

a := Get(3) ?? 10
b := Get(3).OrElse(10)
```

### Enforcing Error Checking

Discarding a `!T` value should be a compile error, as it is in Zig:

```go
Open("test") // error: result of type !File is ignored
```

## Conclusion

The results shown in the Examples and in the Litmus Test are not surprising — we expect this from proper Union Types, both in theory and as demonstrated by multiple languages supporting them.

**Limiting** Union Types to just two special cases is what makes the implementation of this feature much simpler, and perhaps tractable even for a new, quickly growing and evolving group of "systems programming languages".
