Throw, Result, or Neither?

birdculture2 pts0 comments

Throw, Result, or neither? - by Oskar Dudycz

Architecture Weekly

SubscribeSign in

Throw, Result, or neither?

Oskar Dudycz<br>Jul 20, 2026

Share

There is one question I keep getting about my event-sourced code: why don’t I use Result?<br>It usually comes after someone sees a decision throwing an exception. They would put the failure in a Result, leave try/catch at the application boundary, and make the distinction explicit in the return type. That is a fair question, but the return type is only part of the answer.<br>I don’t mind using exceptions. I also return business failures as events. Which one I choose depends on what the failure means and what the application needs to do with it. What’s more, I don’t mind Result either, as long as I’m using a language where it’s a native and idiomatic way to use it. Stil…<br>Some failures are worth retaining as business data. An out-of-stock request can contribute to an unmet-demand report. A declined payment can start another process or be grouped by its reason. An item limit can explain why customers abandon an operation.<br>Gojko Adzic calls the systematic use of unexpected or marginal usage patterns to improve a product lizard optimization . Repeated out-of-stock requests can reveal demand we do not serve or a user experience that encourages impossible quantities. Turning every attempt into only a response or an exception log makes that pattern harder to find. An event gives the application data it can query, project, and use in another process.<br>Event Sourcing can help take advantage of unwanted and unexpected failures. We can model negative outcomes as events, just like regular ones. Our business logic informs us of what happened, not whether it’s a success or an error. It’s just an outcome of our decision.<br>So essentially we’re getting three options to deal with an unexpected scenario.<br>throw an error (e.g. OutOfStockError) and catch it at the boundary;

return a Result with the failure on its error track;

return an event (e.g. ProductItemOutOfStock) and decide separately what we do with it.

Having that, my answer is rarely “just throw” or “just return Result.” Before choosing either, I need to know what should be persisted, what the caller should receive, and whether the failure belongs in the application’s exception path.<br>An error as a business fact

Let’s say that the customer asked for a quantity we cannot supply. Recording that outcome can be useful even though the cart did not change. The cart may also reach its item limit or fail payment authorisation. These are expected business outcomes, and the decision can describe each one with a distinct event:<br>const addProductItem = (<br>command: AddProductItem,<br>state: ShoppingCart,<br>): ProductItemAdded | ProductItemOutOfStock | ShoppingCartItemLimitReached => {<br>if (state.status === "Confirmed")<br>throw new IllegalStateError(<br>"Cannot add a product to a confirmed shopping cart",<br>);

const { shoppingCartId, productItem, availableQuantity, maximumItems, now } =<br>command.data;

if (productItem.quantity > availableQuantity)<br>return {<br>type: "ProductItemOutOfStock",<br>data: {<br>shoppingCartId,<br>productId: productItem.productId,<br>requestedQuantity: productItem.quantity,<br>availableQuantity,<br>attemptedAt: now,<br>},<br>};

if (state.productItems.length >= maximumItems)<br>return {<br>type: "ShoppingCartItemLimitReached",<br>data: {<br>maximumItems,<br>requestedProductId: productItem.productId,<br>},<br>};

return {<br>type: "ProductItemAdded",<br>data: { shoppingCartId, productItem },<br>};<br>};

ProductItemOutOfStock carries the requested and available quantities. ShoppingCartItemLimitReached carries the configured limit and the product that crossed it.<br>We could also model a single ProductItemAddingFailed event instead. It could have a reason string, but then we’d use the granularity and precision of the information. With distinct event types, a projection, workflow, endpoint, or test can check the relevant outcome directly.<br>What’s more, we could also add a FailedToAddProductToClosedShoppingCart and get rid of throwing entirely. We’ll get to that, but for now…<br>Broken rules can throw

Not every failure needs a business event. A confirmed cart can no longer be modified, so AddProductItem is not a valid operation for that state. A command without required data is invalid as well. An unavailable event store, a stock-service timeout, or a serialisation error means the operation could not be completed reliably. I use exceptions for these cases.<br>The throw happens before an event is returned, so the handler has nothing to append. The exception retains its stack and reaches the application’s error boundary.<br>Some people prefer to use Result and hope for deterministic decision-making. Still, in most popular dev environments (TypeScript, Java, C#, Python, etc.), we can get exceptions. Even in strongly-typed Rust, we can get a panic or other cases. In the presented code loadShoppingCart can reject because of data unavailability, appendToStream can report a concurrency or connection error, and...

event result return throw data error

Related Articles