To Throw or Not to Throw
I recently had a pleasant technical discussion during a Delphi hiring process that touched on error-handling style. In one of the exercises, I chose to use a TResult<T> type to keep error handling explicit and predictable. The conversation that followed highlighted how different developers approach this topic.
This post is not about right or wrong. It is about how error-handling practices have evolved, why result types have become increasingly common in modern systems, including those written in traditionally object-oriented languages, and why Result and Maybe (or Option) types are worth considering in Delphi.
A Bit of Historical Context
A prominent example of exception-oriented guidance is Robert C. Martin's Clean Code, published in 2008 and written primarily with Java developers in mind. At the time, Java had no algebraic data types, no standard Option or Result types, and its type system made many functional patterns cumbersome to express.
In that environment, exceptions were often a substantial improvement over brittle error codes, sentinel values, magic return values, and deeply nested conditionals.
But the industry did not stand still.
The Influence of Functional Programming
As functional programming ideas became more mainstream, error handling evolved alongside them. Functional programming places a strong emphasis on predictable behavior, explicit data flow, immutability, and minimizing hidden state.
Exceptions can complicate these goals because they introduce non-local control flow and an additional, implicit path by which a function can terminate.
As functional concepts became more widely adopted, the idea that expected failures should be modeled as values gained traction.
Concurrency
Another major driver of change has been concurrency. As CPU architectures evolved toward multi-core designs, frameworks and programming models increasingly emphasized immutability, reduced shared mutable state, and explicit data flow.
These characteristics make concurrent programs easier to reason about because they reduce the number of hidden interactions between threads and tasks.
Exceptions can also be expensive when they are actually raised, particularly on hot paths, because handling them may involve stack unwinding and additional runtime bookkeeping. This is another reason not to use exceptions as routine control flow.
Result values, by contrast, make expected failure part of the normal execution path and compose naturally across tasks, pipelines, and asynchronous operations. Immutability complements this by removing entire classes of synchronization problems.
These ideas are closely related, although they solve different problems: result-based error handling makes failure explicit, while immutability makes shared state easier to reason about.
Where Modern Languages Landed
This shift is visible across the industry:
- Rust: Result<T, E> and Option<T> are core language idioms.
- Swift: Result and Optional are standard and widely used.
- Kotlin: includes a standard Result<T> type while retaining exceptions as a fundamental error-handling mechanism.
- Go: explicit error returns are the normal convention.
- C++: provides std::optional from C++17 and std::expected from C++23.
- C#: LINQ, lambdas, pattern matching, and other functional features have made value-oriented programming increasingly natural, while Result and Option types are widely available through libraries.
- Java: Java 8 introduced streams, lambdas, functional interfaces, and Optional.
- Dart: does not provide a Result type in its core libraries, but Result-style abstractions are a common application-level pattern.
These languages have not all converged on exactly the same model. Go's explicit error return, Rust's Result, Swift's Result, Java's Optional, and C++'s expected have different semantics and idioms.
What they demonstrate collectively is a broader move toward representing expected outcomes explicitly rather than relying exclusively on exceptional control flow.
Kotlin is an interesting example. It provides a standard Result<T> type while continuing to use exceptions extensively. Its coroutine system also uses exceptions for cancellation.
Rather than replacing exceptions, Kotlin demonstrates how result values and exceptions can serve different purposes within the same language ecosystem.
Result, Maybe and Option
Maybe<T>, Option<T>, and Result<T, E> solve related but different problems.
- Maybe<T> or Option<T> represents the legitimate presence or absence of a value.
- Result<T, E> represents an operation that can explicitly succeed or fail.
- Exceptions remain useful for conditions that fall outside the ordinary result contract.
This distinction is important. An absent value is not necessarily an error, and an expected failure is not necessarily exceptional.
Why Result Types Exist
Result types did not appear in a vacuum. They address weaknesses found in two older approaches:
- primitive return codes such as bool, int, nil, and magic values
- exceptions used for every form of failure, including routine and expected outcomes
Result types preserve the explicitness of return values without the ambiguity of primitive error codes, while avoiding hidden exceptional control flow for expected failures.
They offer several practical advantages:
- Explicit contracts: failure is represented directly in the function's return type.
- Predictable control flow: success and failure paths are visible to the caller.
- Composition: mapping, chaining, and propagating failures can be expressed declaratively.
- Testability: success and failure values can be inspected directly without requiring exception-handling scaffolding.
- Explicit failure propagation: expected failures travel through ordinary values rather than exceptional jumps.
In short, they make expected errors explicit values rather than implicit control flow.
Railway-Oriented Programming
One style commonly associated with result-based error handling is Railway-Oriented Programming.
The metaphor is simple:
- computations proceed along a success track
- failures move execution onto a parallel failure track
- both outcomes are represented explicitly
- failures propagate until something deliberately handles them
Instead of deeply nested conditionals or scattered try/except blocks, operations can be composed declaratively:
- map or transform successful values
- propagate failure by default
- handle errors at an appropriate boundary
This pattern is widely used in functional and functional-influenced systems.
Where Exceptions Struggle
Exceptions are not inherently bad, but they are often overused.
Some practical drawbacks include:
- non-local control flow
- failure paths that are usually invisible in the type system
- runtime cost when exceptions are raised
- more difficult reasoning when exceptional paths become numerous
- easy misuse for routine or expected failures
Exceptions can also blur an important distinction between:
- expected outcome or failure — validation, absence, rejected input, business rules, or other anticipated outcomes
- unexpected failure — broken invariants, programming errors, corruption, or conditions outside the normal contract of an operation
Result types encourage this distinction to be made explicitly as part of the API.
Safety-Critical Systems
In some safety-critical and high-integrity environments, exceptions are prohibited or heavily restricted.
The reasons can include:
- the need for predictable control flow
- constraints around stack unwinding
- difficulty proving or analysing exceptional paths
- static-analysis requirements
- certification and coding-standard constraints
Such environments often favor failure paths that are explicit, analyzable, and bounded. This naturally makes result-based techniques attractive, although the exact rules depend on the language, runtime, coding standard, and certification regime involved.
A Delphi-Specific Nuance
Delphi adds some important context to this discussion:
- there is no built-in Result or Option abstraction in the language or core RTL
- Delphi's exception model is mature and deeply integrated into the ecosystem
- asynchronous and task-based code often relies naturally on exceptions for failure propagation
- cancellation facilities are more limited than those found in some newer async ecosystems
There is also a practical platform consideration. Delphi applications do not operate in isolation. On Windows in particular, operating-system APIs, COM automation, framework code, libraries, and other external components can cause failures that are surfaced to application code as Delphi exceptions.
This means that exceptions cannot simply be eliminated by adopting Result or Option types. Exception handling remains a necessary part of a robust Delphi application, especially at system and integration boundaries.
A useful approach is to treat those boundaries explicitly. An exception originating from an external API may be caught at an appropriate boundary and, when the failure represents an expected application outcome, translated into a Result. Unexpected failures can remain exceptions and propagate to the application's normal exception-handling infrastructure.
Exceptions also remain particularly useful in asynchronous code. Where structured cancellation is unavailable, they can provide a practical mechanism for terminating work and propagating failure across task boundaries.
These are ecosystem constraints worth acknowledging rather than trying to force every failure into a single abstraction.
A Pragmatic Position
A pragmatic approach is to choose the mechanism according to the kind of outcome being represented.
Use Result types for expected failures such as:
- validation failures
- business-rule failures
- other expected error states
Use Maybe or Option when the absence of a value is itself a legitimate outcome.
Use exceptions for conditions such as:
- programming errors
- broken invariants
- unexpected or unrecoverable failures
- cancellation and asynchronous escape paths where the framework naturally uses them
The important point is not to choose one mechanism universally. It is to choose the mechanism whose semantics match the kind of failure being represented.
Closing Thought
Error handling is part of API design.
If an error is part of the domain, it deserves to be modeled explicitly.
Exceptions remain a powerful tool, but they are best used judiciously rather than automatically.
Ultimately, what we are aiming for is intentional API design: choosing how failure is represented based on what that failure means.