<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Exceptions on MongrelDB</title><link>https://www.mongreldb.com/articles/tags/exceptions/</link><description>Recent content in Exceptions on MongrelDB</description><image><title>MongrelDB</title><url>https://www.mongreldb.com/assets/og-mongreldb.png</url><link>https://www.mongreldb.com/assets/og-mongreldb.png</link></image><generator>Hugo</generator><language>en-US</language><lastBuildDate>Tue, 18 Aug 2026 09:00:00 -0500</lastBuildDate><atom:link href="https://www.mongreldb.com/articles/tags/exceptions/index.xml" rel="self" type="application/rss+xml"/><item><title>A Typed Exception Hierarchy That Maps to HTTP</title><link>https://www.mongreldb.com/articles/2026/08/a-typed-exception-hierarchy-that-maps-to-http/</link><pubDate>Tue, 18 Aug 2026 09:00:00 -0500</pubDate><guid>https://www.mongreldb.com/articles/2026/08/a-typed-exception-hierarchy-that-maps-to-http/</guid><description>The MongrelDB PHP client maps HTTP status codes to a small typed exception hierarchy, so a 401 is an AuthException, a 409 is a ConstraintException carrying the server&amp;#39;s error code and the failing operation index, and your catch blocks read like the failure modes they handle.</description><content:encoded><![CDATA[<p>Error handling is where a database client either respects your time or wastes it, because the moment something goes wrong in production you are not reading documentation, you are reading a stack trace, and the difference between &ldquo;the server said 409 because op 3 of your batch violated a unique key&rdquo; and &ldquo;DatabaseException: something failed&rdquo; is the difference between a two-minute fix and an hour of log spelunking. The MongrelDB PHP client speaks HTTP to the daemon, and HTTP already has a perfectly good vocabulary for failure, so rather than inventing a parallel taxonomy of database-flavored error codes, the client maps the status codes it receives onto a small set of typed exceptions, and your catch blocks end up reading like the failure modes they actually handle.</p>
<h2 id="one-base-class-five-shapes-of-failure">One base class, five shapes of failure</h2>
<p>Everything extends <code>MongrelDBException</code>, which extends <code>\Exception</code>, so a single catch at the boundary still works when you do not care about the specifics, and that is the entire hierarchy:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#a6e22e">MongrelDBException</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">├──</span> <span style="color:#a6e22e">ConnectionException</span>   <span style="color:#75715e">// daemon unreachable, network error
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">├──</span> <span style="color:#a6e22e">AuthException</span>         <span style="color:#75715e">// 401 Unauthorized, 403 Forbidden
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">├──</span> <span style="color:#a6e22e">NotFoundException</span>     <span style="color:#75715e">// 404 Not Found
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">├──</span> <span style="color:#a6e22e">ConstraintException</span>   <span style="color:#75715e">// 409 Conflict
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">└──</span> <span style="color:#a6e22e">QueryException</span>        <span style="color:#75715e">// 400 Bad Request, 500 Internal Server Error
</span></span></span></code></pre></div><p>The mapping lives in one place, a <code>match</code> on the response status inside the client, and it is deliberately boring; 401 and 403 both become <code>AuthException</code> even though the fixes differ, a 401 means your credentials were rejected while a 403 means you authenticated fine and simply lack the grant, because from the caller&rsquo;s side both are the same category of problem, an auth-layer failure you cannot retry past, and the message tells you which half of it you are looking at. Where the mapping gets opinionated is 404, which the client detects two ways: a 404 status maps to <code>NotFoundException</code> directly, and on top of that the client inspects the parsed error envelope for a <code>not found:</code> message prefix regardless of status, because &ldquo;that table does not exist&rdquo; deserves its own type even when the server phrases it inside a broader error envelope.</p>
<h2 id="the-one-that-carries-real-weight-constraintexception">The one that carries real weight: ConstraintException</h2>
<p>Most of the hierarchy is just names, but <code>ConstraintException</code> earns its existence by carrying two extra properties, <code>errorCode</code> and <code>opIndex</code>, and those two fields are what turn a batch failure from a mystery into a diff:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Visorcraft\MongrelDB\Exceptions\ConstraintException</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>    $tx <span style="color:#f92672">=</span> $db<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">beginTransaction</span>();
</span></span><span style="display:flex;"><span>    $tx<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">insert</span>(<span style="color:#e6db74">&#39;users&#39;</span>, [<span style="color:#e6db74">&#39;email&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;ada@example.com&#39;</span>, <span style="color:#e6db74">&#39;name&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;Ada&#39;</span>]);
</span></span><span style="display:flex;"><span>    $tx<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">insert</span>(<span style="color:#e6db74">&#39;users&#39;</span>, [<span style="color:#e6db74">&#39;email&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;grace@example.com&#39;</span>, <span style="color:#e6db74">&#39;name&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;Grace&#39;</span>]);
</span></span><span style="display:flex;"><span>    $tx<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">insert</span>(<span style="color:#e6db74">&#39;users&#39;</span>, [<span style="color:#e6db74">&#39;email&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;ada@example.com&#39;</span>, <span style="color:#e6db74">&#39;name&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;Ada again&#39;</span>]);
</span></span><span style="display:flex;"><span>    $tx<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">commit</span>();
</span></span><span style="display:flex;"><span>} <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">ConstraintException</span> $e) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// $e-&gt;errorCode === &#39;UNIQUE_VIOLATION&#39;
</span></span></span><span style="display:flex;"><span>    <span style="color:#75715e">// $e-&gt;opIndex  === 2
</span></span></span><span style="display:flex;"><span>    <span style="color:#75715e">// the whole batch rolled back atomically
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>errorCode</code> is the server&rsquo;s own string code, <code>UNIQUE_VIOLATION</code>, <code>FK_VIOLATION</code>, <code>CHECK_VIOLATION</code>, <code>TRIGGER_VALIDATION</code>, or the generic <code>CONFLICT</code>, and the <code>opIndex</code> tells you which operation inside the atomic batch tripped the constraint, zero-based, so when a forty-operation transaction fails you know exactly which insert to look at instead of bisecting your own code. Because the engine evaluates constraints at commit time and the batch is atomic, a <code>ConstraintException</code> also carries a guarantee you can build on: nothing partially applied, no cleanup pass, the database is exactly where it was before <code>commit()</code> was called, which is the property that makes retrying safe once you have fixed the offending row.</p>
<h2 id="why-status-shaped-exceptions-beat-a-single-databaseexception">Why status-shaped exceptions beat a single DatabaseException</h2>
<p>The old PHP database extensions trained a generation of us to check return values, <code>mysql_query</code> returned <code>false</code> and you called <code>mysql_error()</code> and hoped the string was parseable, and the PDO era improved that to a single exception class with a SQLSTATE code stuffed in <code>getCode()</code>, which meant every serious codebase grew its own switch statement mapping <code>23000</code> to &ldquo;probably a duplicate key.&rdquo; The shape of the failure was always there, it was just encoded in a place the type system could not see, so the compiler could not help you and your IDE could not autocomplete the recovery path. Putting the shape in the class name instead means <code>catch (AuthException)</code> is a complete sentence, it means a static analyzer with unchecked-exception tracking enabled can tell you that you handle constraint violations but not connection failures, and it means the retry policy for a transient <code>ConnectionException</code> never accidentally swallows a permanent <code>QueryException</code>, because those are different types and PHP&rsquo;s catch semantics keep them apart for free.</p>
<p>There is a tradeoff worth naming, and it is that the hierarchy is shallow on purpose; the client does not try to give every server error code its own class, because a deep hierarchy is its own kind of failure, the kind where you version your exceptions and break everyone&rsquo;s catch blocks on a minor release. Five leaves and one rich class is the line we picked, the wire stays the source of truth through <code>errorCode</code> for anything finer-grained, and if the daemon ever grows a genuinely new failure mode that callers must handle differently, that is a deliberate API change and not an accident of a new status string. That is the whole design: HTTP already classified the error, the client just refuses to throw the classification away on the way into PHP.</p>
]]></content:encoded></item></channel></rss>