<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://mehmandarov.com/tag/error-handling/feed.xml" rel="self" type="application/atom+xml"/><link href="https://mehmandarov.com/tag/error-handling/" rel="alternate" type="text/html"/><updated>2026-05-25T10:00:00+02:00</updated><id>https://mehmandarov.com/tag/error-handling/feed.xml</id><title type="html">Rustam Mehmandarov - tag: error handling</title><subtitle type="text">Posts tagged &quot;error handling&quot; on Rustam Mehmandarov.</subtitle><author><name>Rustam Mehmandarov</name></author><entry><title type="html">Sane API error handling with RFC 9457 Problem Details in Jakarta EE</title><link href="https://mehmandarov.com/rfc-9457-problem-details-jakarta-ee/" rel="alternate" type="text/html" title="Sane API error handling with RFC 9457 Problem Details in Jakarta EE"/><published>2026-05-25T10:00:00+02:00</published><updated>2026-05-25T10:00:00+02:00</updated><id>https://mehmandarov.com/rfc-9457-problem-details-jakarta-ee</id><content type="html" xml:base="https://mehmandarov.com/rfc-9457-problem-details-jakarta-ee/"><![CDATA[<p><em>When APIs end up with their own error format, it quickly gets annoying for anyone who has to consume more than one API. <a href="https://www.rfc-editor.org/rfc/rfc9457">RFC 9457</a> defines a standard envelope for HTTP API errors. Let&#8217;s have a look at how to do it in Jakarta EE: a small hand-made <code class="language-plaintext highlighter-rouge">ProblemDetail</code> plus one <code class="language-plaintext highlighter-rouge">ExceptionMapper</code> per error category; with the <a href="https://github.com/zalando/problem">Zalando Problem</a> library; followed by quick notes on Quarkus and Spring as alternatives.</em></p>

<ul>
  <li><a href="#introduction">Introduction</a></li>
  <li><a href="#tldr-why-rfc-9457">TL;DR: Why RFC 9457?</a></li>
  <li><a href="#lets-write-some-code">Let&#8217;s write some code!</a>
    <ul>
      <li><a href="#1-hand-made-problemdetail--exceptionmapper">1. Hand-made <code class="language-plaintext highlighter-rouge">ProblemDetail</code> + <code class="language-plaintext highlighter-rouge">ExceptionMapper</code></a></li>
      <li><a href="#2-zalando-problem">2. Zalando Problem</a></li>
      <li><a href="#3-quarkus-quarkus-http-problem">3. Quarkus: <code class="language-plaintext highlighter-rouge">quarkus-http-problem</code></a></li>
      <li><a href="#4-spring-boot--a-short-note">4. Spring Boot &#8211; a short note</a></li>
    </ul>
  </li>
  <li><a href="#conclusion">Conclusion</a></li>
  <li><a href="#whats-next">What&#8217;s Next?</a></li>
</ul>

<hr />

<h2 id="introduction">Introduction</h2>

<p>If you&#8217;ve consumed more than one or two REST APIs, you&#8217;ve seen the pattern. One service returns <code class="language-plaintext highlighter-rouge">{"error": "..."}</code>, another <code class="language-plaintext highlighter-rouge">{"message": "...", "code": 42}</code>, a third returns <code class="language-plaintext highlighter-rouge">200 OK</code> with an error hidden somewhere deep in the response. Your REST client code fills up with special cases for each one. Sounds familiar?</p>

<p><a href="https://www.rfc-editor.org/rfc/rfc9457">RFC 9457 &#8211; Problem Details for HTTP APIs</a> (the successor to RFC 7807) defines a single JSON envelope for errors, served as <code class="language-plaintext highlighter-rouge">application/problem+json</code> MIME type. It is a small spec: five well-defined bits of information and an <code class="language-plaintext highlighter-rouge">extensions</code> map for anything else you might need.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"urn:problem-type:validation-error"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Validation Failed"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">400</span><span class="p">,</span><span class="w">
  </span><span class="nl">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"The request body or parameters failed validation."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"extensions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"violations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w"> </span><span class="nl">"field"</span><span class="p">:</span><span class="w"> </span><span class="s2">"title"</span><span class="p">,</span><span class="w"> </span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Title is required"</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="tldr-why-rfc-9457">TL;DR: Why RFC 9457?</h2>

<p>Why not keep creating your own?</p>

<ul>
  <li><strong>Consumers already know the shape.</strong> Generated SDKs, gateways, log pipelines, and tracing tools can parse <code class="language-plaintext highlighter-rouge">application/problem+json</code> without extra work.</li>
  <li><strong>You can extend it without breaking clients.</strong> The <code class="language-plaintext highlighter-rouge">extensions</code> map is part of the spec &#8211; put what you need in there.</li>
  <li><strong>It separates the <em>category</em> from the <em>instance</em>.</strong> <code class="language-plaintext highlighter-rouge">type</code> says &#8220;this is a validation error&#8221; (stable, machine-readable); <code class="language-plaintext highlighter-rouge">detail</code> and <code class="language-plaintext highlighter-rouge">instance</code> describe what happened <em>this</em> time.</li>
</ul>

<p>&#128161; <em><strong>Note:</strong> RFC 9457 is just a JSON structure and a content type. No library or framework is required. That&#8217;s why there are so many implementations &#8211; and why a hand-made one is often a reasonable choice.</em></p>

<hr />

<h2 id="lets-write-some-code">Let&#8217;s write some code!</h2>

<p>I have created a repository called <a href="https://github.com/mehmandarov/api-guide-java">API Guide for Java</a> to showcase the patterns for one of my talks. For this post, have a look at <a href="https://github.com/mehmandarov/api-guide-java/blob/main/src/main/java/com/mehmandarov/confapi/error/ProblemDetail.java"><code class="language-plaintext highlighter-rouge">ProblemDetail.java</code></a> and the mappers next to it under <a href="https://github.com/mehmandarov/api-guide-java/tree/main/src/main/java/com/mehmandarov/confapi/error"><code class="language-plaintext highlighter-rouge">com/mehmandarov/confapi/error/</code></a>.</p>

<h3 id="1-hand-made-problemdetail--exceptionmapper">1. Hand-made <code class="language-plaintext highlighter-rouge">ProblemDetail</code> + <code class="language-plaintext highlighter-rouge">ExceptionMapper</code></h3>

<h4 id="what-it-looks-like">What it looks like</h4>

<p>Imagine you have a REST interface looking like this:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@GET</span>
<span class="nd">@Path</span><span class="o">(</span><span class="s">"/{id}"</span><span class="o">)</span>
<span class="nd">@Operation</span><span class="o">(</span><span class="n">summary</span> <span class="o">=</span> <span class="s">"Get room by ID"</span><span class="o">)</span>
<span class="nd">@APIResponse</span><span class="o">(</span><span class="n">responseCode</span> <span class="o">=</span> <span class="s">"200"</span><span class="o">,</span> <span class="n">description</span> <span class="o">=</span> <span class="s">"Room found"</span><span class="o">)</span>
<span class="nd">@APIResponse</span><span class="o">(</span><span class="n">responseCode</span> <span class="o">=</span> <span class="s">"404"</span><span class="o">,</span> <span class="n">description</span> <span class="o">=</span> <span class="s">"Room not found"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">Room</span> <span class="nf">getById</span><span class="o">(</span>
        <span class="nd">@Parameter</span><span class="o">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">"Room ID"</span><span class="o">,</span> <span class="n">required</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>
        <span class="nd">@PathParam</span><span class="o">(</span><span class="s">"id"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">repo</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">id</span><span class="o">)</span>
            <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">NotFoundException</span><span class="o">(</span><span class="s">"Room not found: "</span> <span class="o">+</span> <span class="n">id</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Now, you can add a single <code class="language-plaintext highlighter-rouge">ProblemDetail</code> class &#8211; built around the five RFC 9457 elements and an <code class="language-plaintext highlighter-rouge">extensions</code> map &#8211; and one <a href="https://jakarta.ee/specifications/restful-ws/4.0/apidocs/jakarta.ws.rs/jakarta/ws/rs/ext/exceptionmapper"><code class="language-plaintext highlighter-rouge">ExceptionMapper</code></a> per error category.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">ProblemDetail</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="no">URI</span> <span class="n">type</span> <span class="o">=</span> <span class="no">URI</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="s">"about:blank"</span><span class="o">);</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">title</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kt">int</span> <span class="n">status</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">detail</span><span class="o">;</span>
    <span class="kd">private</span> <span class="no">URI</span> <span class="n">instance</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">extensions</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LinkedHashMap</span><span class="o">&lt;&gt;();</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">ProblemDetail</span> <span class="nf">of</span><span class="o">(</span><span class="kt">int</span> <span class="n">status</span><span class="o">,</span> <span class="nc">String</span> <span class="n">title</span><span class="o">)</span> <span class="o">{</span> <span class="cm">/* ... */</span> <span class="o">}</span>
    <span class="kd">public</span> <span class="nc">ProblemDetail</span> <span class="nf">withType</span><span class="o">(</span><span class="nc">String</span> <span class="n">typeUri</span><span class="o">)</span>            <span class="o">{</span> <span class="cm">/* ... */</span> <span class="o">}</span>
    <span class="kd">public</span> <span class="nc">ProblemDetail</span> <span class="nf">withExtension</span><span class="o">(</span><span class="nc">String</span> <span class="n">key</span><span class="o">,</span> <span class="nc">Object</span> <span class="n">v</span><span class="o">)</span> <span class="o">{</span> <span class="cm">/* ... */</span> <span class="o">}</span>
    <span class="c1">// + getters/setters</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The interesting part is how it gets used. As you can see from the resource code above, there is <strong>no <code class="language-plaintext highlighter-rouge">try/catch</code> in resources, ever</strong> &#8211; every exception is turned into a Problem Details response by an <code class="language-plaintext highlighter-rouge">ExceptionMapper</code>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Provider</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ConstraintViolationExceptionMapper</span>
        <span class="kd">implements</span> <span class="nc">ExceptionMapper</span><span class="o">&lt;</span><span class="nc">ConstraintViolationException</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Response</span> <span class="nf">toResponse</span><span class="o">(</span><span class="nc">ConstraintViolationException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;&gt;</span> <span class="n">violations</span> <span class="o">=</span> <span class="n">ex</span><span class="o">.</span><span class="na">getConstraintViolations</span><span class="o">()</span>
                <span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="k">this</span><span class="o">::</span><span class="n">toMap</span><span class="o">).</span><span class="na">toList</span><span class="o">();</span>

        <span class="nc">ProblemDetail</span> <span class="n">problem</span> <span class="o">=</span> <span class="nc">ProblemDetail</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">400</span><span class="o">,</span> <span class="s">"Validation Failed"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">withType</span><span class="o">(</span><span class="s">"urn:problem-type:validation-error"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">withExtension</span><span class="o">(</span><span class="s">"violations"</span><span class="o">,</span> <span class="n">violations</span><span class="o">);</span>

        <span class="k">return</span> <span class="nc">Response</span><span class="o">.</span><span class="na">status</span><span class="o">(</span><span class="mi">400</span><span class="o">)</span>
                <span class="o">.</span><span class="na">type</span><span class="o">(</span><span class="s">"application/problem+json"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">entity</span><span class="o">(</span><span class="n">problem</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>One mapper per category keeps each file small and obvious: <code class="language-plaintext highlighter-rouge">ConstraintViolationExceptionMapper</code> &#8594; 400, <code class="language-plaintext highlighter-rouge">NotFoundExceptionMapper</code> &#8594; 404, <code class="language-plaintext highlighter-rouge">NotAuthorizedExceptionMapper</code> &#8594; 401, <code class="language-plaintext highlighter-rouge">ForbiddenExceptionMapper</code> &#8594; 403, and a <code class="language-plaintext highlighter-rouge">CatchAllExceptionMapper</code> &#8594; 500 that <strong>never leaks stack traces</strong> to clients.</p>

<p>&#9888;&#65039;<em><strong>A word of caution:</strong> The catch-all mapper is the safety net for everything you forgot to handle. Without one, an uncaught exception ends up in the server&#8217;s default error page, which often includes stack traces, server versions, and sometimes filesystem paths. However, it might be a good idea to handle most of the common exceptions explicitly, and leave the generic catch-all for something truly unexpected.</em></p>

<p><strong>&#9989; Pros:</strong></p>

<ul>
  <li><strong>Portable across runtimes.</strong> The same code runs on Quarkus, Helidon, and Open Liberty. No runtime-specific extension.</li>
  <li><strong>No extra dependencies.</strong> RFC 9457 is just a JSON structure; you don&#8217;t need a library to emit one.</li>
  <li><strong>Small, readable surface.</strong> The error model fits on one slide. When something goes wrong, you can read the source.</li>
</ul>

<p><strong>&#10060; Cons:</strong></p>

<ul>
  <li>You write the boilerplate yourself &#8211; one mapper per category.</li>
  <li>Nothing maps validation, <code class="language-plaintext highlighter-rouge">WebApplicationException</code>, or uncaught <code class="language-plaintext highlighter-rouge">Throwable</code> automatically &#8211; you wire each one up. (This can also be one of the pros, depending on the way you look at things.)</li>
  <li>No content negotiation between <code class="language-plaintext highlighter-rouge">application/json</code> and <code class="language-plaintext highlighter-rouge">application/problem+json</code> unless you add it yourself. (Spring, for example, has a built-in <code class="language-plaintext highlighter-rouge">ProblemDetail</code> that does this for you.)</li>
</ul>

<p>&#128161; <em><strong>Want to know more?</strong> The full code, including all five mappers, lives in <a href="https://github.com/mehmandarov/api-guide-java/tree/main/src/main/java/com/mehmandarov/confapi/error"><code class="language-plaintext highlighter-rouge">com/mehmandarov/confapi/error/</code></a>.</em></p>

<hr />

<h3 id="2-zalando-problem">2. Zalando Problem</h3>

<h4 id="what-it-looks-like-1">What it looks like</h4>

<p>The <a href="https://github.com/zalando/problem">Zalando Problem</a> library (<code class="language-plaintext highlighter-rouge">org.zalando:problem</code> + <code class="language-plaintext highlighter-rouge">jackson-datatype-problem</code>) gives you <code class="language-plaintext highlighter-rouge">Problem</code> and <code class="language-plaintext highlighter-rouge">ThrowableProblem</code> types and Jackson serialization. You still write an <code class="language-plaintext highlighter-rouge">ExceptionMapper</code> to bridge JAX-RS exceptions to <code class="language-plaintext highlighter-rouge">Problem</code>, but you don&#8217;t define the envelope yourself.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">org.zalando.problem.Problem</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.zalando.problem.Status</span><span class="o">;</span>

<span class="nc">Problem</span> <span class="n">problem</span> <span class="o">=</span> <span class="nc">Problem</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
        <span class="o">.</span><span class="na">withType</span><span class="o">(</span><span class="no">URI</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="s">"urn:problem-type:validation-error"</span><span class="o">))</span>
        <span class="o">.</span><span class="na">withTitle</span><span class="o">(</span><span class="s">"Validation Failed"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">withStatus</span><span class="o">(</span><span class="nc">Status</span><span class="o">.</span><span class="na">BAD_REQUEST</span><span class="o">)</span>
        <span class="o">.</span><span class="na">with</span><span class="o">(</span><span class="s">"violations"</span><span class="o">,</span> <span class="n">violations</span><span class="o">)</span>
        <span class="o">.</span><span class="na">build</span><span class="o">();</span>

<span class="k">return</span> <span class="nc">Response</span><span class="o">.</span><span class="na">status</span><span class="o">(</span><span class="mi">400</span><span class="o">)</span>
        <span class="o">.</span><span class="na">type</span><span class="o">(</span><span class="s">"application/problem+json"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">entity</span><span class="o">(</span><span class="n">problem</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
</code></pre></div></div>

<p><strong>&#9989; Pros:</strong></p>

<ul>
  <li><strong>Cross-runtime.</strong> Works on Quarkus, Helidon, and Open Liberty &#8211; the same artifact deploys on all three.</li>
  <li><strong>Used in production at Zalando</strong> (and elsewhere); the model handles <code class="language-plaintext highlighter-rouge">cause</code> chains, stack-trace processing, and a few edge cases you probably would not have thought of upfront.</li>
  <li><strong>Jackson integration is done for you</strong> via <code class="language-plaintext highlighter-rouge">jackson-datatype-problem</code>.</li>
</ul>

<p><strong>&#10060; Cons:</strong></p>

<ul>
  <li>One more dependency to track and upgrade.</li>
  <li>You still write the <code class="language-plaintext highlighter-rouge">ExceptionMapper</code>s &#8211; the library standardises the <em>payload</em>, not the <em>wiring</em>.</li>
  <li>If your stack is JSON-B rather than Jackson, you have a bit of extra work.</li>
</ul>

<hr />

<h3 id="3-quarkus-quarkus-http-problem">3. Quarkus: <code class="language-plaintext highlighter-rouge">quarkus-http-problem</code></h3>

<p>If you&#8217;re <em>only</em> targeting Quarkus, the <a href="https://github.com/quarkiverse/quarkus-http-problem"><code class="language-plaintext highlighter-rouge">quarkus-http-problem</code></a> Quarkiverse extension is the shortest path. It auto-maps <code class="language-plaintext highlighter-rouge">ConstraintViolationException</code>, <code class="language-plaintext highlighter-rouge">WebApplicationException</code>, and uncaught <code class="language-plaintext highlighter-rouge">Throwable</code> to <code class="language-plaintext highlighter-rouge">application/problem+json</code> with no boilerplate from you.</p>

<p><strong>&#9989; Pros:</strong></p>

<ul>
  <li>Add the dependency and you get Problem Details for exceptions. No need to write a mapper for each of them.</li>
  <li>Reasonable defaults for validation and security exceptions.</li>
</ul>

<p><strong>&#10060; Cons:</strong></p>

<ul>
  <li><strong>Quarkus only.</strong> Doesn&#8217;t help on Helidon (Jersey) or Open Liberty (CXF). If &#8220;runs on every Jakarta runtime&#8221; is a requirement, this is out.</li>
  <li>Less visibility into <em>what</em> gets mapped to <em>what</em> &#8211; fine until you need to override a default.</li>
</ul>

<hr />

<h3 id="4-spring-boot--a-short-note">4. Spring Boot &#8211; a short note</h3>

<p>For completeness, we need to mention Spring Boot 3+ as well, which has Problem Details built in as <a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/ProblemDetail.html"><code class="language-plaintext highlighter-rouge">org.springframework.http.ProblemDetail</code></a>, with content negotiation and <code class="language-plaintext highlighter-rouge">@ExceptionHandler</code> integration already wired up. If you&#8217;re on Spring, just use it. The JSON structure is the same RFC 9457; only the wiring differs.</p>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>The point of RFC 9457 is not that there&#8217;s one correct implementation &#8211; there are several reasonable ones &#8211; but that there&#8217;s one correct envelope. Once your API speaks <code class="language-plaintext highlighter-rouge">application/problem+json</code>, clients stop hand-coding error parsers for each new service they consume.</p>

<p>A few rules of thumb:</p>

<ul>
  <li>On <strong>Spring</strong>, use the built-in <code class="language-plaintext highlighter-rouge">ProblemDetail</code>.</li>
  <li>On <strong>Quarkus only</strong>, reach for <code class="language-plaintext highlighter-rouge">quarkus-http-problem</code> and move on.</li>
  <li>For <strong>cross-runtime Jakarta</strong>, choose between <strong>Zalando Problem</strong> (one dependency, more handled for you) and the <strong>hand-made</strong> approach (no dependencies, about 30 lines you fully understand).</li>
</ul>

<p>I picked the hand-made approach for the demo project because portability across Quarkus, Helidon, and Open Liberty mattered, and because the <code class="language-plaintext highlighter-rouge">ExceptionMapper</code> <em>is</em> the demo &#8211; hiding it behind a library would have defeated the point of the talk.</p>

<p>However, &#8220;hand-made&#8221; doesn&#8217;t have to mean &#8220;everyone reinvents it from scratch&#8221;. Write it once, put it in a small internal library, and reuse it across services. That&#8217;s still less code than wiring up a third-party dependency in each runtime.</p>

<h3 id="summary-comparison">Summary Comparison</h3>

<table class="bordered-table">
  <thead>
    <tr>
      <th>Option</th>
      <th>What it gives you</th>
      <th>Runtimes</th>
      <th>Dependency cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Hand-made</strong> <em>(this post)</em></td>
      <td>~30-line <code class="language-plaintext highlighter-rouge">ProblemDetail</code> + one mapper per error category.</td>
      <td>&#160;&#9989; Quarkus &#160;&#9989; Helidon &#160;&#9989; Open Liberty</td>
      <td>None</td>
    </tr>
    <tr>
      <td><strong>Zalando Problem</strong></td>
      <td><code class="language-plaintext highlighter-rouge">Problem</code> / <code class="language-plaintext highlighter-rouge">ThrowableProblem</code> types + Jackson serialization. You still write the mappers.</td>
      <td>&#160;&#9989; Quarkus &#160;&#9989; Helidon &#160;&#9989; Open Liberty</td>
      <td>1&#8211;2 artifacts</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">quarkus-http-problem</code></strong></td>
      <td>Auto-maps validation, <code class="language-plaintext highlighter-rouge">WebApplicationException</code>, and uncaught <code class="language-plaintext highlighter-rouge">Throwable</code>. No boilerplate.</td>
      <td>&#160;&#9989; Quarkus only</td>
      <td>1 extension</td>
    </tr>
    <tr>
      <td><strong>Spring <code class="language-plaintext highlighter-rouge">ProblemDetail</code></strong></td>
      <td>Built into the framework. Content negotiation and <code class="language-plaintext highlighter-rouge">@ExceptionHandler</code> integration.</td>
      <td>&#160;&#9989; Spring Boot 3+</td>
      <td>None (built in)</td>
    </tr>
  </tbody>
</table>

<h2 id="whats-next">What&#8217;s Next?</h2>

<p>Error handling is one of the bonus topics in the <a href="https://github.com/mehmandarov/api-guide-java">API Guide for Java</a>. The same repo also covers OpenAPI documentation, security (RBAC, JWT), pagination, async, and versioning strategies &#8211; see my earlier post on <a href="/api-versioning/">API versioning in Java using JAX-RS</a>.</p>

<p><strong><em>Happy shipping of well-formed error messages, folks!</em></strong></p>

<hr />]]></content><author><name>Rustam Mehmandarov</name></author><summary type="html">A practical look at RFC 9457 Problem Details for HTTP APIs in Jakarta EE &#8211; a hand-made ProblemDetail + ExceptionMapper approach, the Zalando Problem library, and a short note on Quarkus and Spring.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mehmandarov.com/assets/images/posts-images/error.jpg"/><category term="blog"/><category term="english"/><category term="java"/><category term="architecture"/><category term="api"/><category term="jakarta ee"/><category term="microprofile"/><category term="jax-rs"/><category term="error handling"/><category term="quarkus"/><category term="spring"/></entry></feed>
