ADR 0032: C++ error reporting without exceptions¶
Status: Accepted; accessor names amended by ADR 0033, metadata values by ADR 0036
Date: 2026-09-27
Context¶
The header-only C++17 wrapper reported every failure by throwing
postproject::Error. In a few places it threw std::runtime_error or
std::invalid_argument instead. The C ABI is exception-free, but the wrapper
could not be compiled at all with exceptions disabled.
Many C++ hosts disable exceptions:
KDE’s CMake settings build application code with
-fno-exceptions.Unreal Engine and Chromium, and many console and embedded toolchains, forbid exceptions.
Plugin SDKs such as OpenFX must not let exceptions cross their boundaries.
In the first real-host integration, the one adapter file had to be recompiled with exceptions enabled, which is the first thing a maintainer asks about. The C ABI remained available, but it is too verbose to be what PostProject offers a C++ host.
Decision¶
The wrapper is Result-first:
postproject::Result<T>. Every fallible operation returns this C++17 value type, which holds either aTor apostproject::Error. It offersok(),error(), unchecked*and->,valueOr(), andvalue(). The type is[[nodiscard]].Result<void>covers operations that return nothing.Erroris a value. It carries anErrorCodeand a message and is no longer an exception type.One throw, only with exceptions. When exceptions are enabled, detected through
__cpp_exceptionsor MSVC’s_CPPUNWIND,value()throwspostproject::Exception, astd::runtime_errorthat carries theError. Otherwisevalue()on an error prints the error and aborts. No other code in the header throws.One code path per operation. The throwing style is only
value()over the sameResult, so no operation is implemented twice.Deferred errors in builders.
MetadataInputandResolutionOptionsare built fluently. A factory or setter with invalid input records its first error, and the operation that consumes the builder returns it. Literal inputs therefore need no unwrapping.Observer failures.
RevisionObserverreports a failed query as astd::optional<Error>. With exceptions enabled, it also catches an exception thrown by the host’s callback and reports it asErrorCode::internal.
A consumer test builds against the installed package with -fno-exceptions
(/EHs-c- on MSVC).
Alternatives considered¶
A parallel non-throwing API (
tryXnext toX, or anothrownamespace). This doubles the surface and every example, and still leaves a header withthrowexpressions that exception-free builds cannot compile.An error callback or a global error slot. Neither lets the caller handle a failure where it happens, and global state breaks the threading contract of ADR 0014.
std::expected. It needs C++23, and the wrapper targets C++17.
Standards impact¶
None.
Consequences¶
C++ callers change shape: the result of each call is either checked or
unwrapped with .value(), and catch (postproject::Error) becomes
catch (postproject::Exception). Hosts built without exceptions use the
wrapper directly. Builders report invalid input when they are used, not when
they are built.