<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Error-Handling on PHP Boy Scout</title><link>https://blog-570662.gitlab.io/tags/error-handling/</link><description>Recent content in Error-Handling on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Fri, 01 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog-570662.gitlab.io/tags/error-handling/index.xml" rel="self" type="application/rss+xml"/><item><title>Errors without an error handler</title><link>https://blog-570662.gitlab.io/errors-without-an-error-handler/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/errors-without-an-error-handler/</guid><description>&lt;img src="https://blog-570662.gitlab.io/errors-without-an-error-handler/cover-errors-without-an-error-handler.png" alt="Featured image of post Errors without an error handler" /&gt;&lt;p&gt;In &lt;a class="link" href="https://blog-570662.gitlab.io/what-survives-a-port/" &gt;the porting post&lt;/a&gt; I said go-tool-base&amp;rsquo;s error handler was one of the bits that &lt;em&gt;didn&amp;rsquo;t&lt;/em&gt; survive the move to Rust, and promised to come back to it. Here&amp;rsquo;s the come-back. The short version is that Rust hands you, for free, the single consistent error exit that go-tool-base had to build a whole component to get.&lt;/p&gt;
&lt;h2 id="what-go-tool-base-built"&gt;What go-tool-base built
&lt;/h2&gt;&lt;p&gt;A while ago I &lt;a class="link" href="https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/" &gt;wrote about error handling in go-tool-base&lt;/a&gt;. The core of it: an error should carry a &lt;em&gt;hint&lt;/em&gt;, a separate field of human guidance telling the user what to do next, kept apart from the error&amp;rsquo;s identity so code can still match on it.&lt;/p&gt;
&lt;p&gt;The other half of that post was about consistency. Every go-tool-base command returns its errors the idiomatic Cobra way, and they all funnel into one &lt;code&gt;Execute()&lt;/code&gt; wrapper at the root, which routes every error through one &lt;code&gt;ErrorHandler&lt;/code&gt;. One door out. Presentation decided in exactly one place, so no command can render a failure differently from its neighbour.&lt;/p&gt;
&lt;p&gt;That handler is a real object. It exists, it&amp;rsquo;s wired in, it&amp;rsquo;s the thing every error passes through. Building it was a deliberate piece of work, and it was the right call for Go.&lt;/p&gt;
&lt;p&gt;When I rebuilt this in Rust, the handler didn&amp;rsquo;t survive the move. Not because consistency stopped mattering. Because Rust gives you the single exit for free, and an object to enforce it would just be re-implementing something the language already does for you.&lt;/p&gt;
&lt;h2 id="the-shape-of-a-rust-error"&gt;The shape of a Rust error
&lt;/h2&gt;&lt;p&gt;Start with the type. In rust-tool-base every crate defines its own error enum, and every one of them derives two traits:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[derive(Debug, thiserror::Error, miette::Diagnostic)]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;ConfigError&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;config file not found at {path}&amp;#34;&lt;/span&gt;&lt;span class="cp"&gt;)]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;#[diagnostic(
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; code(rtb::config::not_found),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; help(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;run `mytool init` to create one, or set MYTOOL_CONFIG&amp;#34;&lt;/span&gt;&lt;span class="cp"&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; )]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NotFound&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;: &lt;span class="nc"&gt;PathBuf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;thiserror::Error&lt;/code&gt; makes it a proper error type. &lt;code&gt;miette::Diagnostic&lt;/code&gt; is the interesting one. A &lt;code&gt;Diagnostic&lt;/code&gt; is an error that also carries the things you&amp;rsquo;d want when &lt;em&gt;presenting&lt;/em&gt; it: a stable &lt;code&gt;code&lt;/code&gt;, a severity, a &lt;code&gt;help&lt;/code&gt; string, and optionally source labels pointing at spans of input. The &lt;code&gt;help&lt;/code&gt; line is the same idea as go-tool-base&amp;rsquo;s hint, the recovery step, except here it&amp;rsquo;s an attribute on the variant rather than a field threaded through a wrapper.&lt;/p&gt;
&lt;p&gt;So the guidance lives on the error, structured, from the moment the error is created.&lt;/p&gt;
&lt;h2 id="there-is-no-handler-theres-a-convention"&gt;There is no handler, there&amp;rsquo;s a convention
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s where Rust does the work go-tool-base&amp;rsquo;s handler was built to do.&lt;/p&gt;
&lt;p&gt;A rust-tool-base &lt;code&gt;main&lt;/code&gt; looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[tokio::main]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nc"&gt;miette&lt;/span&gt;::&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rtb&lt;/span&gt;::&lt;span class="n"&gt;cli&lt;/span&gt;::&lt;span class="n"&gt;Application&lt;/span&gt;::&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VersionInfo&lt;/span&gt;::&lt;span class="n"&gt;from_env&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;main&lt;/code&gt; returns &lt;code&gt;miette::Result&amp;lt;()&amp;gt;&lt;/code&gt;. Every command&amp;rsquo;s &lt;code&gt;run&lt;/code&gt; returns a &lt;code&gt;Result&lt;/code&gt; too. In between, errors propagate with the &lt;code&gt;?&lt;/code&gt; operator: a function that hits an error returns it upward, immediately, and the caller does the same, all the way to &lt;code&gt;main&lt;/code&gt;. Nobody writes a &amp;ldquo;check this error&amp;rdquo; call. &lt;code&gt;?&lt;/code&gt; is the propagation.&lt;/p&gt;
&lt;p&gt;And when an error reaches &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;main&lt;/code&gt; returns it, &lt;em&gt;something&lt;/em&gt; has to render it for the user. That something is a report hook. rust-tool-base installs one at startup, and from then on any &lt;code&gt;Diagnostic&lt;/code&gt; that exits &lt;code&gt;main&lt;/code&gt; is rendered through it: the code, the severity, the help text, the source labels, with colour. One renderer, installed once.&lt;/p&gt;
&lt;p&gt;Look at what that adds up to. Every error in the program flows to one place, &lt;code&gt;main&lt;/code&gt;. It&amp;rsquo;s rendered by one thing, the hook. Presentation is decided in exactly one location and no command can deviate from it. That&amp;rsquo;s precisely the property go-tool-base&amp;rsquo;s &lt;code&gt;ErrorHandler&lt;/code&gt; was built to guarantee. The difference is that nobody built it. The single exit is just where &lt;code&gt;?&lt;/code&gt; propagation ends, and the single renderer is one hook. The language&amp;rsquo;s own convention for returning errors from &lt;code&gt;main&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; the funnel.&lt;/p&gt;
&lt;h2 id="errors-are-values-all-the-way"&gt;Errors are values, all the way
&lt;/h2&gt;&lt;p&gt;The thing that took me a moment to fully trust is that there&amp;rsquo;s no funnel to maintain, because there&amp;rsquo;s no funnel as an object. go-tool-base&amp;rsquo;s handler is a component: it can drift, it has to be kept in the path, a command could in principle be wired to bypass it. The Rust version cannot be bypassed, because bypassing it would mean a command not returning its error, and an error you don&amp;rsquo;t return is a compile-time warning at best and dead-obvious wrong code at worst.&lt;/p&gt;
&lt;p&gt;So the model is just: errors are values, you return them, &lt;code&gt;?&lt;/code&gt; carries them up, &lt;code&gt;main&lt;/code&gt; hands the last one to the hook. The consistency isn&amp;rsquo;t enforced by a guard. It&amp;rsquo;s the only thing the shape of the language really lets you do.&lt;/p&gt;
&lt;p&gt;go-tool-base reaches a single, consistent error exit by building one and routing everything through it. rust-tool-base reaches the same exit by having errors be ordinary return values and letting them fall out of &lt;code&gt;main&lt;/code&gt;. Same outcome. One of them is a component you own; the other is a convention you inherit.&lt;/p&gt;
&lt;h2 id="worth-remembering"&gt;Worth remembering
&lt;/h2&gt;&lt;p&gt;go-tool-base funnels every error through one &lt;code&gt;ErrorHandler&lt;/code&gt; so presentation stays consistent. That handler is a deliberately built component, and it&amp;rsquo;s the right design in Go.&lt;/p&gt;
&lt;p&gt;rust-tool-base has no handler. Every crate&amp;rsquo;s error type derives &lt;code&gt;miette::Diagnostic&lt;/code&gt;, carrying its code, severity and help text. Errors propagate with &lt;code&gt;?&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt;, which returns &lt;code&gt;miette::Result&lt;/code&gt;, and a framework-installed hook renders whatever comes out. The single consistent exit is the end of &lt;code&gt;?&lt;/code&gt; propagation, and the single renderer is one hook. The funnel go-tool-base built by hand is, in Rust, just the language&amp;rsquo;s return-from-&lt;code&gt;main&lt;/code&gt; convention.&lt;/p&gt;</description></item><item><title>Errors that tell the user what to do next</title><link>https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/</link><pubDate>Sun, 22 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/</guid><description>&lt;img src="https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/cover-errors-that-tell-the-user-what-to-do-next.png" alt="Featured image of post Errors that tell the user what to do next" /&gt;&lt;p&gt;Here&amp;rsquo;s an error message I&amp;rsquo;ve been on the receiving end of more times than I&amp;rsquo;d care to count:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;error: failed to read config file
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;True. Also completely useless! I now know something is broken and I haven&amp;rsquo;t the faintest idea what to do about it. Which file? Why couldn&amp;rsquo;t it be read? Should I create it, run some &lt;code&gt;init&lt;/code&gt; command, fix a permission, set an environment variable? The message states the problem and then abandons me at it, rather like a sat-nav cheerfully announcing &amp;ldquo;you have arrived&amp;rdquo; in the middle of a motorway.&lt;/p&gt;
&lt;h2 id="a-message-is-not-a-fix"&gt;A message is not a fix
&lt;/h2&gt;&lt;p&gt;The instinct, the moment you notice this, is to go and write a &lt;em&gt;better message&lt;/em&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="o"&gt;~/.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mytool&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Run&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;mytool init&amp;#39;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;MYTOOL_CONFIG&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Better for the human, no question. But look at what you&amp;rsquo;ve just done to the error as a &lt;em&gt;value&lt;/em&gt;. The recovery advice is now welded into the error string. Any code that wants to ask &amp;ldquo;is this the config-missing error?&amp;rdquo; is reduced to substring-matching English prose. Reword the advice and you break the check. So you&amp;rsquo;ve helped the user and quietly sabotaged the program at the same time, because you&amp;rsquo;ve made one poor little string do two completely incompatible jobs&amp;hellip; being a stable identity for code, and being friendly guidance for people.&lt;/p&gt;
&lt;h2 id="why-i-changed-error-libraries"&gt;Why I changed error libraries
&lt;/h2&gt;&lt;p&gt;go-tool-base started out on &lt;code&gt;github.com/go-errors/errors&lt;/code&gt;. It&amp;rsquo;s a perfectly fine library and it gave us stack traces. What it didn&amp;rsquo;t give us was any way to attach human guidance to an error without shoving it into the message string. So the codebase did exactly the daft thing I just described: multi-line suggestion text baked straight into &lt;code&gt;errors.Errorf&lt;/code&gt; calls, user-facing content and programmatic identity all mashed into one value.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the whole reason for the migration to &lt;code&gt;github.com/cockroachdb/errors&lt;/code&gt;. Not novelty, and not because I fancied a weekend of find-and-replace. One specific capability: &lt;code&gt;cockroachdb/errors&lt;/code&gt; lets you attach a &lt;strong&gt;hint&lt;/strong&gt; to an error as a separate, structured field.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithHint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;failed to read config file&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Run &amp;#39;mytool init&amp;#39; to create one, or set MYTOOL_CONFIG to point at an existing file.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now there are two things, cleanly apart. &lt;code&gt;errors.New(&amp;quot;failed to read config file&amp;quot;)&lt;/code&gt; is the &lt;em&gt;identity&lt;/em&gt;&amp;hellip; stable, matchable, the program&amp;rsquo;s handle on the error. The hint is the &lt;em&gt;guidance&lt;/em&gt;&amp;hellip; for the human, and rewordable as much as you like without breaking a single check, because no check ever looks at it. &lt;code&gt;errors.Is&lt;/code&gt; and &lt;code&gt;errors.As&lt;/code&gt; work properly through every wrapper layer, so code matches on identity and never has to read prose.&lt;/p&gt;
&lt;p&gt;The migration brought a few other things worth having. Stack traces print with a plain &lt;code&gt;%+v&lt;/code&gt; instead of a type assertion. Errors can carry structured, machine-readable metadata. Multiple errors from concurrent work can be combined as a first-class value. But the hint is the one that actually changed the user&amp;rsquo;s day, because the hint is the recovery step, stored where it belongs.&lt;/p&gt;
&lt;h2 id="one-door-out-and-it-knows-where-the-help-is"&gt;One door out, and it knows where the help is
&lt;/h2&gt;&lt;p&gt;Separating the hint is only half of it. The other half is making sure those hints actually reach the user, every time, and that comes down to having a single way out.&lt;/p&gt;
&lt;p&gt;Every go-tool-base command returns its errors the idiomatic Cobra way, through &lt;code&gt;RunE&lt;/code&gt;. They all funnel into one &lt;code&gt;Execute()&lt;/code&gt; wrapper at the root, which routes every error (runtime failure, flag parse error, pre-run failure) through one &lt;code&gt;ErrorHandler&lt;/code&gt;. One door out. So error &lt;em&gt;presentation&lt;/em&gt; gets decided in exactly one place, and no command can render an error differently from the command sat next to it.&lt;/p&gt;
&lt;p&gt;And because there&amp;rsquo;s one handler, it can pull off something the individual commands never could. The framework knows your tool&amp;rsquo;s metadata, including its configured support channel, be it a Slack workspace or a Teams channel. So the error handler can finish a fatal error not just with the &lt;em&gt;what&lt;/em&gt; and the recovery hint, but with &lt;em&gt;where to go if the hint didn&amp;rsquo;t help&lt;/em&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Run&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;mytool init&amp;#39;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;MYTOOL_CONFIG&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Still&lt;/span&gt; &lt;span class="n"&gt;stuck&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Ask&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="c1"&gt;#mytool-support on Slack.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The user is never left at a dead end. The error tells them what broke, the hint tells them the most likely fix, and if that&amp;rsquo;s still not enough the handler tells them which door to go and knock on. A failure becomes a signpost instead of a full stop.&lt;/p&gt;
&lt;h2 id="the-short-version"&gt;The short version
&lt;/h2&gt;&lt;p&gt;An error that only reports what went wrong leaves the user stranded, and the obvious fix (writing the recovery advice into the message) quietly wrecks the error as a value, because now your code has to substring-match prose just to work out what it&amp;rsquo;s looking at.&lt;/p&gt;
&lt;p&gt;go-tool-base moved from &lt;code&gt;go-errors&lt;/code&gt; to &lt;code&gt;cockroachdb/errors&lt;/code&gt; to get hints: a structured, separate field for human guidance that leaves the error&amp;rsquo;s identity clean for &lt;code&gt;errors.Is&lt;/code&gt; and &lt;code&gt;errors.As&lt;/code&gt;. Every command&amp;rsquo;s errors leave through one &lt;code&gt;Execute()&lt;/code&gt; wrapper and one &lt;code&gt;ErrorHandler&lt;/code&gt;, so presentation stays consistent, and because that handler knows the tool&amp;rsquo;s support channel it can point a stuck user at real help.&lt;/p&gt;
&lt;p&gt;State the problem for the program. Give the fix to the human. And for pity&amp;rsquo;s sake, keep the two in different fields.&lt;/p&gt;</description></item></channel></rss>