<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Dimagog]]></title><description><![CDATA[Dimagog]]></description><link>https://dimagog.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 26 Oct 2026 12:58:01 GMT</lastBuildDate><atom:link href="https://dimagog.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Some functions can both fail and return nothing]]></title><description><![CDATA[In the previous post 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…]]></description><link>https://dimagog.hashnode.dev/some-functions-can-both-fail-and-return-nothing</link><guid isPermaLink="true">https://dimagog.hashnode.dev/some-functions-can-both-fail-and-return-nothing</guid><category><![CDATA[programming languages]]></category><category><![CDATA[error handling]]></category><dc:creator><![CDATA[Dmitry Kakurin]]></dc:creator><pubDate>Mon, 26 Oct 2026 05:38:15 GMT</pubDate><content:encoded><![CDATA[<p>In the <a href="https://dimagog.hashnode.dev/better-error-handling-with-just-two-union-types">previous post</a> we've limited Union Types to just two kinds: <code>?T</code> for <code>T | None</code> and <code>!T</code> for <code>T | Error</code>. One of the corollaries was that <code>?!T</code>/<code>!?T</code> is not supported, because it would mean <code>T | None | Error</code>, and we strive for simplicity.</p>
<p>But a lookup that can both fail <em>and</em> find nothing is common enough:</p>
<pre><code class="language-go">// the DB may be down (Error), or the key may be absent (None)
func Find(key: string) !?Record
</code></pre>
<p>Let's see what it takes to support <code>!?T</code> without giving up on simplicity.</p>
<h2>The obvious options</h2>
<ol>
<li><strong>Treat "not found" as an <code>Error</code></strong> and return <code>!T</code>. This is what Go does with <code>sql.ErrNoRows</code> and <code>fs.ErrNotExist</code>. "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 <code>try</code> propagates "not found" up the stack as if something went wrong.</li>
<li><strong>Keep the layers</strong>, as Zig does: <code>!?T</code> is an error wrapping an optional, unwrapped one layer at a time. It works, but every caller has to unwrap twice, and <code>?!T</code> ends up being a different type from <code>!?T</code>, which violates the "Union Types are sets" approach.</li>
</ol>
<h3>Tempting option 3: Derive <code>None</code> from <code>Error</code></h3>
<p>If <code>None</code> is a subtype of <code>Error</code>, then <code>?T</code> is implicitly convertible to <code>!T</code>, and <code>T | None | Error</code> is the same as <code>T | Error</code>. In other words, <code>!?T</code> is just <code>!T</code>. We get a simple chain <code>T &lt;: ?T &lt;: !T</code>, and the nesting problem goes away without any new types.</p>
<p>There are many benefits to this approach:</p>
<ul>
<li><code>Find</code> simply returns <code>!T</code>, and "not found" is <code>none</code>, which is now a valid <code>Error</code> value. The caller checks <code>if r := Find(key) {} else if r is None {} else { log(r) }</code>.</li>
<li>The boolean context rule becomes simpler: a value is falsy if it is <code>false</code> or <code>Error</code>, and truthy otherwise.</li>
<li>Changing a return type from <code>!T</code> to <code>?T</code> is now a narrowing, so it passes the Litmus Test from the previous post.</li>
<li><code>try</code> needs no extra rules: <code>try</code> on <code>?T</code> works inside functions returning <code>?U</code> or <code>!U</code>, and <code>try</code> on <code>!T</code> only inside functions returning <code>!U</code>.</li>
</ul>
<p>But there is one problem, and it is a big one: <code>none</code> becomes an error without any context. <code>var f: !File = none</code> compiles, and <code>log(f)</code> has nothing useful to print. Worse, <code>try m.Get(key)</code> inside a function returning <code>!Config</code> propagates <code>none</code> to the caller instead of saying which key was missing, and nothing in the type system reminds you to do better.</p>
<p>This is not a theoretical concern. Rust tried exactly this: <code>std::option::NoneError</code> on nightly allowed <code>?</code> to convert an <code>Option</code> into a <code>Result</code>, and it was removed for this very reason. Zig requires an explicit conversion instead: <code>orelse error.NotFound</code>. And Go's <code>io.EOF</code> is the same idea in practice: an "error" that is really an expected absence, with <code>if err == io.EOF</code> checks everywhere.</p>
<p>More fundamentally, half of the argument in the Go post was that the absence of a result is not an error. Making <code>None</code> an <code>Error</code> gives that up for <code>!T</code>. We want <code>None</code> and <code>Error</code> to remain distinct types, so the compiler knows which one we meant.</p>
<h2>Better option: Sealed Common Base</h2>
<p>Instead, let's derive both <code>None</code> and <code>Error</code> from a common base type <code>Failure</code>:</p>
<pre><code class="language-go">sealed type Failure = None | Error
</code></pre>
<p><code>Failure</code> is sealed, i.e., <code>None</code> and <code>Error</code> are its only subtypes. Now the Union Type algebra does the rest:</p>
<ul>
<li><code>??T</code> is the same as <code>?T</code>, because <code>T | None | None</code> is <code>T | None</code></li>
<li><code>!!T</code> is the same as <code>!T</code>, because <code>T | Error | Error</code> is <code>T | Error</code></li>
<li><code>!?T</code> is the same as <code>?!T</code>, because both are <code>T | None | Error</code>, which is <code>T | Failure</code></li>
</ul>
<p>Nesting flattens, the order does not matter, and <code>!?T</code> is the only new type. We still have just two <em>primitive</em> kinds of Union Types, and nesting them produces the third one. Let's use <code>!?T</code> as the canonical spelling.</p>
<h3>Conversions</h3>
<p>Every type is implicitly convertible to the types below it, but not the other way around:</p>
<pre><code class="language-text">Error  T  None
   | /   \ |
   !T     ?T
     \   /
      !?T
</code></pre>
<p>In code:</p>
<pre><code class="language-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
</code></pre>
<p>The last two lines are the whole point of not going with option 3: <code>None</code> and <code>Error</code> remain distinct, and the compiler will enforce it.</p>
<h2>Boolean context and narrowing</h2>
<p>The rule from the previous post actually becomes simpler: a value is falsy if it is <code>false</code> or a <code>Failure</code>, and truthy otherwise.</p>
<p>Narrowing works the same way: in the <code>else</code> branch <code>!?T</code> is narrowed to <code>Failure</code>, and one more type check tells <code>None</code> and <code>Error</code> apart:</p>
<pre><code class="language-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)
}
</code></pre>
<p>For comparison, Zig needs both <code>catch</code> and <code>orelse</code> for the same thing.</p>
<h2><code>try</code> and <code>!?T</code></h2>
<p>The <code>try</code> 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:</p>
<table>
<thead>
<tr>
<th><code>try</code> applied to</th>
<th>inside <code>?U</code> func</th>
<th>inside <code>!U</code> func</th>
<th>inside <code>!?U</code> func</th>
</tr>
</thead>
<tbody><tr>
<td><code>?T</code></td>
<td>✅</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td><code>!T</code></td>
<td>❌</td>
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td><code>!?T</code></td>
<td>❌</td>
<td>❌</td>
<td>✅</td>
</tr>
</tbody></table>
<p>Converting between kinds has to be explicit:</p>
<pre><code class="language-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)
}
</code></pre>
<p>With option 3 the first line would have compiled, and the caller would have received an error saying nothing.</p>
<h2>Litmus Test</h2>
<p>Let's run the same test as in the previous post: which signature changes do not break the callers?</p>
<h3>Extending Inputs</h3>
<p>Changing a parameter type from <code>?int</code> to <code>!?int</code> is a widening, so old callers passing an <code>int</code> or <code>none</code> still work. Whether a function should ever accept an <code>Error</code> as input is a separate question, but the type system does not need to forbid it.</p>
<p><strong>Test passed!</strong></p>
<h3>Restricting Outputs</h3>
<p><code>!?T</code> can be narrowed in two directions: to <code>!T</code> or to <code>?T</code>, and then further to <code>T</code>. Both paths are backward compatible, same as before.</p>
<p><strong>Case 1: <code>!?T</code> -&gt; <code>!T</code>, no more <code>none</code>.</strong> Suppose we've learned how to never return <code>none</code> from <code>Find</code>:</p>
<pre><code class="language-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)
}
</code></pre>
<p><strong>Test passed!</strong> Note that in the dead <code>else if</code> branch <code>r</code> is of type <code>Error</code>, so <code>r is None</code> is always <code>false</code>. Perhaps the compiler generates an "always false" warning, similar to the "always true" warning in the previous post.</p>
<p><strong>Case 2: <code>!?T</code> -&gt; <code>?T</code>, no more errors.</strong> Now suppose instead that <code>Find</code> never has to return errors anymore:</p>
<pre><code class="language-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
}
</code></pre>
<p><strong>Test passed!</strong> Here the compiler is less likely to notice the dead <code>else</code> branch, since <code>else</code> has no condition to evaluate. It also does not change anything for <code>try</code>: <code>try Find(key)</code> inside a function returning <code>!U</code> was rejected before (<code>Find</code> could return <code>none</code>) and is still rejected after.</p>
<p>Changing <code>!T</code> to <code>?T</code> 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.</p>
<h2>What does it cost?</h2>
<ul>
<li><strong>A third type.</strong> It is derived rather than declared, but it exists, and <code>!?T</code> will show up in function signatures.</li>
<li><strong>Two-step narrowing.</strong> Telling <code>None</code> from <code>Error</code> takes a second <code>if</code>. Zig has the same cost.</li>
</ul>
<h2>Conclusion</h2>
<p>In <a href="https://dimagog.hashnode.dev/why-go-style-error-handling-is-not-great">Why Go-style error handling is not great</a> we've called a function <strong>partial</strong> if it is unable to produce a return value for all possible inputs. <code>T | Failure</code> is exactly that: either a value, or the reason there is none. <code>?T</code> and <code>!T</code> cover the two possible reasons, and <code>!?T</code> covers both.</p>
<p>Two primitive Union Types and one sealed base type are all it takes to support nesting.</p>
]]></content:encoded></item><item><title><![CDATA[Better error handling with just two Union Types]]></title><description><![CDATA[In previous posts I've covered my beef with error handling in some programming languages: But what can we do without going all the way to full-blown Union…]]></description><link>https://dimagog.hashnode.dev/better-error-handling-with-just-two-union-types</link><guid isPermaLink="true">https://dimagog.hashnode.dev/better-error-handling-with-just-two-union-types</guid><category><![CDATA[error handling]]></category><category><![CDATA[programming languages]]></category><category><![CDATA[zig]]></category><category><![CDATA[ziglang]]></category><dc:creator><![CDATA[Dmitry Kakurin]]></dc:creator><pubDate>Sun, 25 Oct 2026 21:03:42 GMT</pubDate><content:encoded><![CDATA[<p>In previous posts I've covered my beef with error handling in some programming languages:</p>
<ul>
<li><a href="https://dimagog.hashnode.dev/why-go-style-error-handling-is-not-great">Why Go-style error handling is not great</a></li>
<li><a href="https://medium.com/@dmitrykakurin/why-crystal-and-typescript-have-the-best-union-types-1a8fbfba831">Why TypeScript and Crystal have the best Union Types</a></li>
</ul>
<p>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 <code>int | string</code> and are willing to limit our needs to optional values and the possibility of functions failing?</p>
<p>Let's take our inspiration from <a href="https://ziglang.org/">Zig</a>, which already does almost exactly this, and use the same <code>?T</code> and <code>!T</code> notation. However, we won't follow Zig 1:1. Also note that Kotlin, Swift, Dart, and C# have the <code>?T</code> half written as <code>T?</code>.</p>
<p>It's worth spelling out what exactly makes it work, and what it takes to pass the API evolution Litmus Test at the end.</p>
<h2>1: Limited Union Types</h2>
<p>The basic idea is to introduce just two possible kinds of Union Types:</p>
<ol>
<li>For a type <code>T</code>, <code>?T</code> means <code>T | None</code>.</li>
<li>For a type <code>T</code>, <code>!T</code> means <code>T | Error</code>. Here <code>Error</code> is assumed to be a single interface type, like Go's <code>error</code>, i.e., no typed errors.</li>
</ol>
<p>Corollaries:</p>
<ol>
<li>It follows from #1 above that <code>??T</code> and <code>?T</code> are the same type because <code>T | None | None</code> is <code>T | None</code>.</li>
<li>The desire to have just two Union Types precludes <code>?!T</code>/<code>!?T</code>: it would mean <code>T | None | Error</code>, and we strive for simplicity <em>(but wait till you read <a href="https://dimagog.hashnode.dev/some-functions-can-both-fail-and-return-nothing">the next post</a>)</em>.</li>
</ol>
<p><strong>Very importantly</strong>, the following pseudocode should be accepted by the compiler (this implicit conversion is what <code>Maybe</code>/<code>Result</code> wrappers lack, and what makes the Litmus Test below pass):</p>
<pre><code class="language-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
</code></pre>
<p>And the following should still be rejected:</p>
<pre><code class="language-go">x = maybe_x // ?T is not convertible to T
x = error_x // !T is not convertible to T
</code></pre>
<p>In other words, <code>T</code> is a proper subtype of <code>?T</code> and <code>!T</code>.</p>
<h2>2: Extending boolean context</h2>
<p>In a boolean context, e.g., inside an <code>if</code> condition:</p>
<ol>
<li><code>?T</code> is truthy if it is <code>T</code> and falsy if it is <code>None</code></li>
<li><code>!T</code> is truthy if it is <code>T</code> and falsy if it is <code>Error</code></li>
</ol>
<h2>3: Control-Flow-Sensitive typing</h2>
<p>TypeScript calls it <strong>Type Narrowing</strong>, Crystal — <strong>Type Restriction</strong>, and Scala — <strong>Flow Typing</strong>. But the idea is the same: <em>at compile time</em>, a variable's type becomes more precise depending on code flow.</p>
<p>In our case, a variable of type <code>?T</code> or <code>!T</code> can be narrowed to type <code>T</code>, and to <code>None</code>/<code>Error</code> in the else branch.</p>
<h2>Examples combining it all together</h2>
<p>An iterator returning the next element:</p>
<pre><code class="language-go">func Next() ?T

for val := Next() {
    // even though Next returns ?T, here val has the type T
}
</code></pre>
<p>Opening a file:</p>
<pre><code class="language-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)
}
</code></pre>
<h2>Function signature evolution Litmus Test</h2>
<p>This is the same test that <code>Maybe</code>/<code>Result</code> wrappers fail in <a href="https://medium.com/@dmitrykakurin/why-crystal-and-typescript-have-the-best-union-types-1a8fbfba831">Why TypeScript and Crystal have the best Union Types</a>.</p>
<p>Imagine <code>v1</code> of our library had a function:</p>
<pre><code class="language-go">func f(x: int) ?string
</code></pre>
<p>Now we are going to publish <code>v2</code> of this lib. What can we do to this interface without breaking the callers?</p>
<h3>Extending Inputs</h3>
<p>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 <code>x</code>, and we'd like to make <code>x</code> optional. Can we change the type of <code>x</code> to <code>?int</code>?</p>
<pre><code class="language-go">func f(x: ?int) ?string

// old callers still work
f(3)

// but now this is possible too
f(none)
</code></pre>
<p><strong>Test passed!</strong></p>
<h3>Restricting Outputs</h3>
<p>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 <code>string</code> from <code>f</code>, and it does not have to be optional anymore, as we never return <code>none</code>. Can we change the return type to <code>string</code>?</p>
<pre><code class="language-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()
</code></pre>
<p><strong>Test almost passed</strong>: we need to define how values of regular types (like <code>string</code> here) are evaluated in a boolean context. A simple rule could be: everything is truthy, except bool <code>false</code>, <code>None</code>, and <code>Error</code>. However, there are wrinkles when narrowing functions returning <code>bool</code>. Going from returning <code>?bool</code> to <code>bool</code> changes the semantics for older callers: they would take the "no value" branch when <code>false</code> is returned. But that's an acceptable limitation — we just postulate that such an interface change is not backward compatible; don't do it.</p>
<h2>Error Handling Ergonomics</h2>
<p>Two more things are needed in practice, and both are orthogonal to the type system.</p>
<h3>Error Propagation Syntactic Sugar</h3>
<p>Since <code>Error</code> converts implicitly to any <code>!T</code>, passing an error up the stack already works:</p>
<pre><code class="language-go">func Load(name: string) !Config {
    if f := Open(name) {
        return Parse(f)
    } else {
        return f // f is Error here
    }
}
</code></pre>
<p>But as Yogi Berra said, "It's déjà vu all over again". It's not that different from Go's <code>if err != nil { return err }</code>.</p>
<p>A <code>try</code> prefix (as in Zig) or a <code>?</code> postfix (as in Rust) that returns the <code>Error</code> to the caller, and otherwise narrows to <code>T</code>, removes the boilerplate.</p>
<p>With a <code>try</code> prefix:</p>
<pre><code class="language-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)
}
</code></pre>
<p>or with a <code>?</code> postfix and no local variable:</p>
<pre><code class="language-go">func Load(name: string) !Config {
    return Parse(Open(name)?)
}
</code></pre>
<p>Similarly, supplying a default value for <code>?T</code> can be done with a <code>??</code>/<code>orelse</code> operator or an <code>OrElse</code> method.</p>
<pre><code class="language-go">func Get(i: int) ?int

a := Get(3) ?? 10
b := Get(3).OrElse(10)
</code></pre>
<h3>Enforcing Error Checking</h3>
<p>Discarding a <code>!T</code> value should be a compile error, as it is in Zig:</p>
<pre><code class="language-go">Open("test") // error: result of type !File is ignored
</code></pre>
<h2>Conclusion</h2>
<p>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.</p>
<p><strong>Limiting</strong> 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".</p>
]]></content:encoded></item><item><title><![CDATA[Why Go-style error handling is not great]]></title><description><![CDATA[There is an important concept in software development called "correct by construction" 1. Briefly, it means incorrect things cannot be expressed using a…]]></description><link>https://dimagog.hashnode.dev/why-go-style-error-handling-is-not-great</link><guid isPermaLink="true">https://dimagog.hashnode.dev/why-go-style-error-handling-is-not-great</guid><category><![CDATA[Go Language]]></category><category><![CDATA[golang]]></category><category><![CDATA[programming languages]]></category><category><![CDATA[error handling]]></category><dc:creator><![CDATA[Dmitry Kakurin]]></dc:creator><pubDate>Sun, 25 Oct 2026 00:36:20 GMT</pubDate><content:encoded><![CDATA[<p>There is an important concept in software development called <em>"correct by construction"</em> <a href="#heading-notes"><sup>1</sup></a>. Briefly, it means incorrect things cannot be expressed using a language/library/config/etc. in question. And implies that the very fact that you can construct something means it's correct.</p>
<p>Go-style error handling violates this principle. Let's see how.</p>
<h2>Why bother?</h2>
<p>Go is a well-established language, and despite overwhelming user feedback, even the core team gave up on improving error handling after multiple attempts (although they were mostly considering syntactic sugar to address endless <code>if err != nil</code>). Am I really hoping to suggest a change that is going to be implemented in Go?</p>
<p>No, and it's not the goal here. The goal is to learn from Go, and to stop repeating its mistakes, because many brand-new "systems programming languages" are replicating Go's approach, and it's a shame (at least from my perspective).</p>
<h2>How Go deals with partial functions</h2>
<p>A function is called <strong>partial</strong> if it is unable to produce a return value for all possible inputs. Then the question is: how does it indicate to the caller that the value was not produced?</p>
<p>Go has 2 standard patterns for dealing with such situations.</p>
<h3>Absence of a result</h3>
<p>Getting the map value and checking if the map contains a key in one call needs a way to indicate both "here is the value" and "key not found". Go's way of dealing with such cases is returning a tuple, where the second <code>bool</code> value indicates success/failure:</p>
<pre><code class="language-go">val, ok := m["key"]
if !ok { ... key not found }
</code></pre>
<h3>Error performing an operation</h3>
<p>Opening a file could fail for many reasons. So, in addition to the file handle, Go prescribes returning an <code>error</code> value, where <code>nil</code> indicates "no error":</p>
<pre><code class="language-go">file, err := os.Open("example.txt")
if err != nil { ... failed with err }
</code></pre>
<h2>What's wrong with this approach?</h2>
<p>In both cases the language allows you to express more cases than actually needed/desired. We'd like to express just 2 possible outcomes:</p>
<ul>
<li>key found, here is the value / key not found</li>
<li>file opened, here is the handle / here is a specific error why the file cannot be opened</li>
</ul>
<p>But mechanically a 2x2 matrix of 4 outcomes can be expressed, where 1 case makes no sense, and often 2 cases:</p>
<table>
<thead>
<tr>
<th>⬇️val / ok ➡️</th>
<th>false</th>
<th>true</th>
</tr>
</thead>
<tbody><tr>
<td><code>nil</code> / default val</td>
<td>cannot produce val</td>
<td>sometimes meaningful<br />often not</td>
</tr>
<tr>
<td>meaningful val</td>
<td>BUG</td>
<td>val produced</td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th>⬇️val / err ➡️</th>
<th>set</th>
<th><code>nil</code></th>
</tr>
</thead>
<tbody><tr>
<td><code>nil</code> / default val</td>
<td>error has happened</td>
<td>sometimes meaningful<br />often not</td>
</tr>
<tr>
<td>meaningful val</td>
<td>BUG<a href="#heading-notes"><sup>2</sup></a></td>
<td>val produced</td>
</tr>
</tbody></table>
<p>It brings unnecessary questions, like:</p>
<pre><code class="language-go">// which one is better?
// (1) defer first:
file, err := os.Open("example.txt")
defer file.Close()
if err != nil { ... failed with err }

// or (2) check error first
file, err := os.Open("example.txt")
if err != nil { ... handle err }
defer file.Close()
</code></pre>
<p>Go prescribes <code>(2)</code> to be used, but it means a function returning <strong>both</strong> a file handle and an error will leak handles. Sure, it's a bug in that function, but that's <strong>exactly the point here</strong>: if it were impossible to express, this class of bugs would not exist.</p>
<h3>Nothing forces you to check</h3>
<p>Both patterns are conventions, not rules. The language happily lets you skip the check:</p>
<pre><code class="language-go">val := m["key"]          // key absent? val is 0, no error, no warning
f, _ := os.Open("x.txt") // compiles clean, f is nil on failure
</code></pre>
<p>Go complains about unused <em>variables</em>, never about unused <em>results</em>. That is why <code>errcheck</code> exists.</p>
<h2>Can we do better?</h2>
<p>With the introduction of generics, Go can follow many other programming languages by using <code>Maybe[T]</code> for the first case and <code>Result[T]</code> (implying failure type is always <code>error</code>) for the second.</p>
<p>There are 2 improvements here:</p>
<ol>
<li><p>Invalid state cannot be expressed. That's correct by construction.</p>
</li>
<li><p>Value <strong>must</strong> be unwrapped before it can be used, so no forgetting to check for errors:</p>
<pre><code class="language-go">// assume m is map[string]int
val := m["key"] // val's type is Maybe[int]
x := val + 1 // would not compile, val is not an int
</code></pre>
<pre><code class="language-go">file := os.Open("example.txt") // file's type is Result[*os.File]
file.Read(buffer) // would not compile, file is not an *os.File
</code></pre>
</li>
</ol>
<h2>Is this the best we can do?</h2>
<p>In Go — perhaps, but in general <strong>no, we can do better</strong>. And I've covered it before: <a href="https://dmitrykakurin.medium.com/why-crystal-and-typescript-have-the-best-union-types-1a8fbfba831?sharedUserId=dmitrykakurin">Why TypeScript and Crystal have the best Union Types</a>.</p>
<p>However, there is also a middle ground: not quite full Union Types, but still better than <code>Maybe</code>/<code>Result</code>. This is the subject of the <a href="https://dimagog.hashnode.dev/better-error-handling-with-just-two-union-types">next post</a>.</p>
<hr />
<h3>Notes</h3>
<ol>
<li>Also known as "make illegal states unrepresentable", a phrase coined by Yaron Minsky in <a href="https://blog.janestreet.com/effective-ml-revisited/">Effective ML Revisited</a> (2011).</li>
<li>Go's <a href="https://pkg.go.dev/io#Reader"><code>io.Reader</code> contract</a> does this on purpose: it allows <code>Read</code> to return <code>n &gt; 0</code>, <code>io.EOF</code>. And callers must process the bytes before looking at the error. <strong>This is beyond good and evil, this is just diabolical.</strong></li>
</ol>
]]></content:encoded></item></channel></rss>