Skip to content

refactor: Add util::Result failure types and ability to merge result values#25665

Open
ryanofsky wants to merge 7 commits intobitcoin:masterfrom
ryanofsky:pr/bresult2
Open

refactor: Add util::Result failure types and ability to merge result values#25665
ryanofsky wants to merge 7 commits intobitcoin:masterfrom
ryanofsky:pr/bresult2

Conversation

@ryanofsky
Copy link
Contributor

@ryanofsky ryanofsky commented Jul 21, 2022

Add util::Result support for returning more error information and make use of it in LoadChainstate method as an initial application. Followup PRs #25722 and #29700 use it more broadly to return errors and warnings from wallet and kernel functions as well.

This change adds two major features to the result class:

  • For better error handling, adds the ability to return a value on failure, not just a value on success. This is a key missing feature that makes the result class not useful for functions like LoadChainstate() which produce different errors that need to be handled differently 1.
  • For better error reporting, adds the ability to return warning messages and multiple errors, not just a single error string. This provides a way for functions to report errors and warnings in a standard way, and simplifies interfaces:

    -virtual std::unique_ptr<Wallet> loadWallet(const std::string& name, bilingual_str& error, std::vector<bilingual_str>& warnings) = 0;
    +virtual util::Result<std::unique_ptr<Wallet>> loadWallet(const std::string& name) = 0;
    -std::unique_ptr<WalletDatabase> MakeDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error);
    +util::Result<std::unique_ptr<WalletDatabase>, DatabaseError> MakeDatabase(const fs::path& path, const DatabaseOptions& options);

This change also makes some more minor improvements:

  • Smaller type size. util::Result<int> is 16 bytes, and util::Result<void> is 8 bytes. Previously both were 72 bytes.

  • Broader type compatibility. Supports noncopyable and nonmovable success and error types.

Comparison with std::expected

Currently, the util::Result class is useful for error reporting—returning translated, user-facing error strings—but not as useful for error handling, where callers need structured failure information to decide how to proceed. By contrast, std::expected / util::Expected (#34005, #34006) is good for error handling but less helpful for reporting. This PR closes the gap between the two classes.

With this PR, std::expected remains a good choice for lower-level functions or simple functions that fail in only a few well-defined ways. Meanwhile,

  • Result becomes a better choice for operations that can fail in many different ways—loading a wallet, connecting a block, etc.—where callers may want complete error traces, not just low-level errors without context or high-level errors without detail. (Followup #25722 applies this to wallet-loading functions.)
  • Result can also be more useful than expected when merging status information from multiple calls. (Followup #29700 uses this to reliably propagate flush and fatal-error status from validation code that can internally trigger flushes or shutdowns.)
  • Result may also be a better choice than std::expected when returning pointer values. (Followup #26022 adds a ResultPtr specialization that avoids double dereferencing.)
  • Result can be more efficient than std::expected when failures are rare but failure objects are large, since failure values are heap-allocated.

This PR effectively makes Result a superset of expected, making the representations compatible and allowing them to be used together.

Footnotes

  1. Ability to return error values was actually present in the original implementation of #25218, but unfortunately removed in later iterations. See design notes.

@ryanofsky
Copy link
Contributor Author

Draft PR since I want to add a commit taking advantages of ability to return chain results and return warnings.

@DrahtBot
Copy link
Contributor

DrahtBot commented Jul 21, 2022

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage & Benchmarks

For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/25665.

Reviews

See the guideline for information on the review process.

Type Reviewers
Concept ACK arejula27
Approach ACK hebasto
Stale ACK w0xlt, stickies-v, hernanmarino, jonatack, maflcko, laanwj, achow101, sedited

If your review is incorrectly listed, please copy-paste <!--meta-tag:bot-skip--> into the comment that the bot should ignore.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #34641 (node: scale default -dbcache with system RAM by l0rinc)
  • #34589 (ci: Temporarily use clang in valgrind tasks by maflcko)
  • #34544 (wallet: Disallow wallet names that are paths including .. and . elements by achow101)
  • #34132 (coins: drop error catcher, centralize fatal read handling by l0rinc)
  • #32061 (Replace libevent with our own HTTP and socket-handling implementation by pinheadmz)
  • #28690 (build: Introduce internal kernel library by sedited)
  • #26022 (Add util::ResultPtr class by ryanofsky)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

LLM Linter (✨ experimental)

Possible typos and grammar issues:

  • "defines helper functions types that can be used with util::Result class to for dealing with error and warning messages." -> "defines helper function types that can be used with the util::Result class for dealing with error and warning messages." ["functions types" is incorrect; also "to for dealing" is redundant/incorrect and needs rephrasing for clarity.]

  • "allowing each to be understood to changed more easily." -> "allowing each to be understood or changed more easily." ["to changed" is ungrammatical; likely intended "or changed" or "to be changed".]

  • "Helper function to join messages in space separated string." -> "Helper function to join messages in a space-separated string." [missing article and hyphenation makes the phrase slightly unclear.]

  • "The source and destination results are not required to have the same types, and assigning void source types to non-void destinations type is allowed, since no source information is lost." -> "The source and destination results are not required to have the same types, and assigning void source types to non-void destination types is allowed, since no source information is lost." ["destinations type" is incorrect in number/possessive; should be "destination types".]

Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

  • ExpectSuccess(IntFn(5, true), {}, 5) in src/test/result_tests.cpp
  • ExpectSuccess(NoCopyFn(5, true), {}, 5) in src/test/result_tests.cpp
  • ExpectError(NoCopyFn(5, false), Untranslated("nocopy 5 error."), 5) in src/test/result_tests.cpp
  • ExpectSuccess(NoCopyNoMoveFn(5, true), {}, 5) in src/test/result_tests.cpp
  • ExpectError(NoCopyNoMoveFn(5, false), Untranslated("nocopynomove 5 error."), 5) in src/test/result_tests.cpp
  • ExpectSuccess(MultiWarnFn(3), Untranslated("warn 0. warn 1. warn 2."), 3) in src/test/result_tests.cpp
  • ExpectSuccess(AccumulateFn(true), Untranslated("warn 0. warn 0. warn 1. int 3 warn."), 3) in src/test/result_tests.cpp
  • ExpectSuccess(TruthyFalsyFn(0, true), {}, 0) in src/test/result_tests.cpp
  • ExpectError(TruthyFalsyFn(0, false), Untranslated("error value 0."), 0) in src/test/result_tests.cpp
  • ExpectSuccess(TruthyFalsyFn(1, true), {}, 1) in src/test/result_tests.cpp
  • ExpectError(TruthyFalsyFn(1, false), Untranslated("error value 1."), 1) in src/test/result_tests.cpp
  • ExpectSuccess(result, {}, 0) in src/test/result_tests.cpp
  • ExpectSuccess(result2, {}, 0) in src/test/result_tests.cpp

If you want, I can propose specific inline comment names for each literal to improve readability.

2026-03-02 02:56:06

@hodlinator
Copy link
Contributor

At this point I would prefer util::Expected be merged first (#34006).

I think util::Result is conflating 2 related but orthogonal needs:

  • Returning a successful value or error(s).
  • Returning multiple structured errors/warnings, not just one.

Maybe it would be clearer to use something like util::Expected<V, util::ErrorReport> directly in "user code". With...

class ErrorReport
{
    std::unique_ptr<detail::Messages> m_data;
    ...

...to fulfill the wish to keep memory usage low for the happy path.
(Is there some case where we want to return a successful value together with a warning?)


An alternative would be to implement util::Result as a subclass of util::Expected as an intermediate step.

Compiling example based upon fa1eee4f46fd089d6e19b124addee7c7679585f4 from #34006.
diff --git a/src/util/expected.h b/src/util/expected.h
index 38baef4d21..bf8fa2f25c 100644
--- a/src/util/expected.h
+++ b/src/util/expected.h
@@ -7,6 +7,7 @@
 
 #include <attributes.h>
 
+#include <cassert>
 #include <variant>
 
 namespace util {
diff --git a/src/util/result.h b/src/util/result.h
index 39c224f61b..fe00bf3b41 100644
--- a/src/util/result.h
+++ b/src/util/result.h
@@ -5,11 +5,9 @@
 #ifndef BITCOIN_UTIL_RESULT_H
 #define BITCOIN_UTIL_RESULT_H
 
-#include <attributes.h>
+#include <util/expected.h>
 #include <util/translation.h>
 
-#include <variant>
-
 namespace util {
 
 struct Error {
@@ -31,14 +29,10 @@ struct Error {
 //! `std::optional<T>` can be updated to return `util::Result<T>` and return
 //! error strings usually just replacing `return std::nullopt;` with `return
 //! util::Error{error_string};`.
-template <class M>
-class Result
+template <class V, typename E = bilingual_str>
+class Result : public Expected<V, E>
 {
 private:
-    using T = std::conditional_t<std::is_same_v<M, void>, std::monostate, M>;
-
-    std::variant<bilingual_str, T> m_variant;
-
     //! Disallow copy constructor, require Result to be moved for efficiency.
     Result(const Result&) = delete;
 
@@ -49,50 +43,31 @@ private:
     Result& operator=(const Result&) = delete;
     Result& operator=(Result&&) = delete;
 
-    template <typename FT>
-    friend bilingual_str ErrorString(const Result<FT>& result);
+    template <typename T>
+    friend bilingual_str ErrorString(const Result<T, bilingual_str>& result);
 
 public:
-    Result() : m_variant{std::in_place_index_t<1>{}, std::monostate{}} {}  // constructor for void
-    Result(T obj) : m_variant{std::in_place_index_t<1>{}, std::move(obj)} {}
-    Result(Error error) : m_variant{std::in_place_index_t<0>{}, std::move(error.message)} {}
+    using Expected<V, E>::Expected;
+    constexpr Result(Error error) requires(std::is_same_v<E, bilingual_str>) : Expected<V, E>{Unexpected<E>{std::move(error.message)}} {}
     Result(Result&&) = default;
     ~Result() = default;
 
-    //! std::optional methods, so functions returning optional<T> can change to
-    //! return Result<T> with minimal changes to existing code, and vice versa.
-    bool has_value() const noexcept { return m_variant.index() == 1; }
-    const T& value() const LIFETIMEBOUND
-    {
-        assert(has_value());
-        return std::get<1>(m_variant);
-    }
-    T& value() LIFETIMEBOUND
-    {
-        assert(has_value());
-        return std::get<1>(m_variant);
-    }
     template <class U>
-    T value_or(U&& default_value) const&
+    V value_or(U&& default_value) const&
     {
-        return has_value() ? value() : std::forward<U>(default_value);
+        return this->has_value() ? this->value() : std::forward<U>(default_value);
     }
     template <class U>
-    T value_or(U&& default_value) &&
+    V value_or(U&& default_value) &&
     {
-        return has_value() ? std::move(value()) : std::forward<U>(default_value);
+        return this->has_value() ? std::move(this->value()) : std::forward<U>(default_value);
     }
-    explicit operator bool() const noexcept { return has_value(); }
-    const T* operator->() const LIFETIMEBOUND { return &value(); }
-    const T& operator*() const LIFETIMEBOUND { return value(); }
-    T* operator->() LIFETIMEBOUND { return &value(); }
-    T& operator*() LIFETIMEBOUND { return value(); }
 };
 
 template <typename T>
-bilingual_str ErrorString(const Result<T>& result)
+bilingual_str ErrorString(const Result<T, bilingual_str>& result)
 {
-    return result ? bilingual_str{} : std::get<0>(result.m_variant);
+    return result ? bilingual_str{} : result.error();
 }
 } // namespace util
 

Then from that point, more advanced util::Result changes could be made, like switching the default error type from bilingual_str to std::unique_ptr<util::Messages>:

template <class V, typename E = std::unique_ptr<Messages>>
class Result : public Expected<V, E>

or if preferred:

template <class V>
class Result : public Expected<V, std::unique_ptr<Messages>>

@ryanofsky
Copy link
Contributor Author

re: #25665 (comment)

Thanks for taking a look at this.

I think util::Result is conflating 2 related but orthogonal needs:

  • Returning a successful value or error(s).
  • Returning multiple structured errors/warnings, not just one.

The result class is handling both of these things (since c++ works most conveniently with a single return value), but I don't see a sense in which it is conflating them. The implementation handles them independently accepting FailureType and MessagesType as separate template parameters which are both customizable:

template <typename SuccessType = void, typename FailureType = void, typename MessagesType = Messages>
class Result;

You suggestion to use a util::Expected<V, util::ErrorReport> type could clearly work as well, but if I think if you look at current uses of the Result class in existing code, or at intended uses in #25722 and #29700, you can see how it would make calls more awkward and verbose.

Your suggestions to make the Result class inherit from the expected class, or use it internally, or use value types instead of unique_ptr, also probably could be implemented, but it's not clear to me what they would be accomplishing. They seem like relatively small implementation changes that would not affect the Result API or relate to the main goals of this PR which are to: (1) give the existing Result class feature-parity and compatibility with expected and (2) give it the ability to merge results together, which is useful for high level functions (loading a wallet, connecting a block) that perform many operations internally and can return a cumulative status instead of a single one-shot value.

I think if an expected class gives you all the error-handlng functionality you need, it's great to use, especially in lower-level utility code. But in higher level wallet and validation code more types of failures are possible and the Result class can be a better fit.

@arejula27
Copy link

arejula27 commented Dec 6, 2025

Concept ACK [f0aff63]
This PR was started in 2022, before the C++23 standard introduced std::expected. I agree with @hodlinator that this PR attempts to solve two different issues:

  1. The error-handling workflow
  2. The ability to merge results, which is useful for high-level functions (e.g., loading a wallet, connecting a block) that perform many internal operations and may want to return a cumulative status instead of a single one-shot value

I agree that these are interesting problems to address. However, I believe the first one is already solved by utils::expected (and i suggest merging first the #34006 pr and then this one with the required modifications to adapt to it) and will be solved by std::expected in the future.

This PR has been open for three years, and it is clear that a lot of work and constant updates have gone into it. Still, I feel that merging another utils::expected like mechanism would be redundant regarding error-handling workflow. That said, I think a small refactor could help to merge both: removing the Result class while keeping components like FailDataHolder, which could then be used inside utils::expected.

One of the most appealing aspects of the current utils::Result is the ErrorString helper, as shown in the example:

void TryAddNumbers(int a, int b)
    {
        if (auto result = AddNumbers(a, b)) {
            LogPrintf("%i + %i = %i\n", a, b, *result);
        } else {
            LogPrintf("Error: %s\n", util::ErrorString(result).translated);
        }
    }

This feels smooth and does not add verbosity. However, I would also like to consider an approach like this:

template <typename T, typename E>
requires requires(const E& e) { e.messages; }   // has .messages
bilingual_str ErrorString(const utils::expected<T,E>& result)
{
    if (result.has_value())
        return bilingual_str{};

    const Messages& m = result.error().messages;
    return detail::JoinMessages(m);
}

If you agree, this direction could be interesting, I can continue exploring the idea and evaluating how viable it is. I doubt the snippet I provided would compile as-is, it is only meant to illustrate the proposal.

Another good option (as mentioned earlier in the discussion) would be to base the Result type on expected.

template <class V>
class Result : public Expected<V, std::unique_ptr<Messages>>

This would isolate all error-handling mechanics in the expected class, while keeping reporting, updating, and merging logic inside Result.

Finally, I would like to mention that clearer documentation would be valuable. It would be helpful to specify when this class should be used, as the author mentioned several use cases beyond simply reporting errors to the user.


#include <util/result.h>

#include <algorithm>
Copy link

@arejula27 arejula27 Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this include and it continue compiling, are we sure this is required? I executed the following commands:

➜  bitcoin (25665) % git log -1 --format=%H                                                                                                           17:21
f0aff63b5ad51566e626d5b24eee08eb81df54a1
➜  bitcoin (25665) % git diff src/util/result.cpp                                                                                                     17:21
diff --git a/src/util/result.cpp b/src/util/result.cpp
index ea79375aa3..423618e85d 100644
--- a/src/util/result.cpp
+++ b/src/util/result.cpp
@@ -4,7 +4,6 @@

 #include <util/result.h>

-#include <algorithm>
 #include <initializer_list>
 #include <iterator>
 #include <util/translation.h>
➜  bitcoin (25665) % rm -rf build                                                                                                                     17:22
➜  bitcoin (25665) % cmake -B build > /dev/null 2>&1                                                                                                  17:22
➜  bitcoin (25665) % cmake --build build -j "$(($(nproc)/2))" > /dev/null 2>&1                                                                        17:22
➜  bitcoin (25665) % ./build/bin/test_bitcoin                                                                                                         17:22
Running 686 test cases...

*** No errors detected

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually treat missing or extra includes as nits. There is an iwyu run as part of the tidy CI job. See the logs here: https://github.com/bitcoin/bitcoin/actions/runs/19279986116/job/55128814542?pr=25665#step:9:13821 .

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that I should not review includes? I am starting to contribute, sorry if this is not relevant

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is important to you, it is enough to link the respective lines in the iwyu report.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx🫡, I guess this conversation can be marked as resolved then

Copy link
Contributor

@hodlinator hodlinator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #25665 (comment)

Excuse me, I somehow glossed over the fact that you had 3 template parameters in this PR, not 2. 🤦 I missed that you allowed for a FailureType apart from MessageType, the former defaulting to void but messages being default-on. I see a greater justification for the distinct Result type. However, it seems the either/or aspect remains, if I'm not mistaken.

Inspired by your reply, I nerd-sniped myself into experimenting with a branch based on #25722 to check my assumptions. I'm able to replace Result with raw Expected in most cases, but wallet code forces me to use:

template <typename T, typename FailureType = void>
using ResultLog = Expected<T, ErrorLog<FailureType>>;

The benefit I see to using Expected even as part of "ResultLog" is that expected is a known pattern in C++, so it would slot into people's understanding. From there it comes down providing sufficient ergonomics of mutating/merging when using ErrorLog.

using T = std::conditional_t<std::is_same_v<M, void>, std::monostate, M>;

std::variant<bilingual_str, T> m_variant;
//! tuple<variant<SuccessType, FailureType>, MessagesType>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in e78cfb3 "refactor: Add util::Result failure values":

Isn't it closer to:

Suggested change
//! tuple<variant<SuccessType, FailureType>, MessagesType>
//! tuple<optional<SuccessType>, unique_ptr<tuple<FailureType, MessagesType>>>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #25665 (comment)

Isn't it closer to: tuple<optional<SuccessType>, unique_ptr<tuple<FailureType, MessagesType>>>

The current comment here says "Logically, a result object is equivalent to tuple<variant<SuccessType, ErrorType>, MessagesType>".

This is just trying to get across the idea that a Result holds a success value or a failure value, plus some messages.

IMO, adding unique_ptr to this description would make it more confusing (and the specific suggestion would also not be accurate since failure and message values can be set independently). Use of unique_ptr is an implementation detail not exposed to callers. The heap allocation is relevant since it could have performance implications, so it's mentioned in the comment. But it's not important for understanding what information is stored in the class or how the class can be used.

util::Result<void> result{RemoveTxs(batch, txs_to_remove)};
if (!result) str_err = util::ErrorString(result);
return result.has_value();
return bool{result};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remark: Took a few minutes to realize that has_value() doesn't exist because of the void template specialization of SuccessHolder which doesn't have that. Makes sense.

//! to void destination types is not allowed, since this would discard
//! source information.
template <bool DstConstructed, typename DstResult, typename SrcResult>
static void Move(DstResult& dst, SrcResult& src)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would anything be gained from making the template parameter an r-value? (Earlier commits do explicit std::move()s, justifying the function's name, but that justification appears to have dissipated).

Suggested change
static void Move(DstResult& dst, SrcResult& src)
static void Move(DstResult& dst, SrcResult&& src)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #25665 (comment)

Would anything be gained from making the template parameter an r-value? (Earlier commits do explicit std::move()s, justifying the function's name, but that justification appears to have dissipated).

There isn't a great way of forcing this to be an rvalue, since just replacing & && would make it a universal reference and make using the deduced SrcResult type more awkward. I also don't think it would be too helpful in any case, since this is not a public function and there are only 2 internal callers. Using a plain reference here is the simplest approach, I believe.

// 3. Path to a symlink to a directory.
// 4. For backwards compatibility, the name of a data file in -walletdir.
const fs::path wallet_path = fsbridge::AbsPathJoin(GetWalletDir(), fs::PathFromString(name));
fs::path wallet_path = fsbridge::AbsPathJoin(GetWalletDir(), fs::PathFromString(name));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit Q in 3c535e2 "wallet: fix clang-tidy warning performance-no-automatic-move":
I got the impression our expectation is that all commits should pass CI. So I would expect this change to come before or in the same commit that would cause CI failure. Is that only valid for the HEAD commit when it comes to commits that resolve clang-tidy and similar checks?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #25665 (comment)

nit Q in 3c535e2 "wallet: fix clang-tidy warning performance-no-automatic-move": I got the impression our expectation is that all commits should pass CI. So I would expect this change to come before or in the same commit that would cause CI failure. Is that only valid for the HEAD commit when it comes to commits that resolve clang-tidy and similar checks?

I don't think there's a rule about this, but yes I wouldn't choose a commit only fixing a clang-tidy warning (that seems very brittle anyway) to be the first change here, since it seems easier to understand as a followup. Could reorder commits though if you or anyone else feels this is important.

Comment on lines +164 to +167
//! Container for SuccessType, providing public accessor methods similar to
//! std::optional methods to access the success value.
template <typename SuccessType, typename FailureType, typename MessagesType>
class SuccessHolder : public FailDataHolder<FailureType, MessagesType>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing the reason for the existence of a separate SuccessHolder type is in order to specialize away a minimal subset of functionality in SuccessHolder<void, ...>. If that's part of the reason, it could be admitted in the comment block for the main SuccessHolder template?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #25665 (comment)

I'm guessing the reason for the existence of a separate SuccessHolder type is in order to specialize away a minimal subset of functionality in SuccessHolder<void, ...>. If that's part of the reason, it could be admitted in the comment block for the main SuccessHolder template?

Yes exactly. Added a comment mentioning the void type to make this clearer.

Comment on lines +53 to +55
util::Result<kernel::InterruptResult, ChainstateLoadError> LoadChainstate(ChainstateManager& chainman, const kernel::CacheSizes& cache_sizes,
const ChainstateLoadOptions& options);
util::Result<kernel::InterruptResult, ChainstateLoadError> VerifyLoadedChainstate(ChainstateManager& chainman, const ChainstateLoadOptions& options);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An example from my larger experimentation showed that Expected can be used to implement a variant of the chainstate refactor from this PR (0ad3a45), see self-contained commit hodlinator@b40a36c.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I maintain that the current example in this PR of applying util::Result to LoadChainstate & VerifyLoadedChainstate is not well motivated as they don't use warnings or multiple errors, and are thus a better fit for util::Expected as in my linked commit.

It would be better to use a different example in this PR unless I'm still missing something.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it could use a struct instead of a raw std::pair, but otherwise, if std::expected or util::Expected work out of the box here, it seems easier to just use them?

Copy link
Contributor Author

@ryanofsky ryanofsky Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if std::expected or util::Expected work out of the box here

They don't. Hodlinator is suggesting replacing:

  • Result<InterruptResult, ChainstateLoadError>

with:

  • Expected<InterruptResult, pair<ChainstateLoadError, bilingual_str>>.

So using pair + Expected together to replicate functionality of Result.

This does work fine (it's just slightly more verbose) in cases like this example where you have one function or a set of functions that each produce their own error messages and need to return those messages to callers. But it falls apart when the functions call other functions that also produce error messages and those errors need to be forwarded or merged (#25722, #29700).

The point of Result is to be a superset of Expected and drop-in replacement that provides:

  • An implicit messages field so you do not need specify bilingual_str everywhere and can avoid pair<X, bilingual_str>, make_pair boilerplate.
  • Merging operators and methods (>>, update) so when Result functions call other Result functions, complete error messages can be returned with information about low level failures and higher level context.
  • Merging hooks so Result functions calling other Result functions can bubble up other information as well, such as fatal/nonfatal error status and flush status in #29700.

Expected is a good way to return simple status information from low-level C++ functions. Result is an extension of Expected intended to be used for high level functions which can fail in complicated ways and should be able to return detailed descriptive messages. (It also supports returning warnings which are used heavily in wallet code, though not currently in the kernel.)

Copy link
Contributor

@hodlinator hodlinator Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning both error enum value and error string is a bit unusual. It seems one could reduce to the more conventional Expected<InterruptResult, ChainstateLoadError> by slightly increasing the number of ChainstateLoadError-enum members and having a separate mapping from error enum value -> error string.

nop

There's only one strprintf that would lead to actually loosing information I realized this doesn't rely on local information, so could also be moved:

return {ChainstateLoadStatus::FAILURE, strprintf(_("Witness data for blocks after height %d requires validation. Please restart with -reindex."),
chainman.GetConsensus().SegwitHeight)};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning both error enum value and error string is a bit unusual.

Right, that is why Result second parameter defaults to void. The point is that generally Result should be a better choice than Expected when you do want to return error messages.

It seems one could reduce to the more conventional Expected<InterruptResult, ChainstateLoadError> by slightly increasing the number of ChainstateLoadError-enum members and having a separate mapping from error enum value -> error string.

Disagree that enums are a good way to return error messages from high level code. This results in programs telling you things like "permissions error" without telling you which resource couldn't be accessed or "syntax error" without telling you where the problem occurred. If you want to return helpful error messages, you need to a way to construct them in them places where context is available, and a way to bubble them up to callers, and this is what the Result class provides. Or we could just go with bool and "Oops, something went wrong!" 😉

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we could just go with bool and "Oops, something went wrong!" 😉

Don't start me on GenericError being used everywhere. :)

If you want to return helpful error messages, you need to a way to construct them in them places where context is available

To some extent util::Result and util::Expected both compete with Rust's std::Result which is empowered by Rust enums being able to contain error strings. I guess defining the untranslated error string at the place in the code that triggers the error should make for the best messages.

This does work fine (it's just slightly more verbose) in cases like this example where you have one function or a set of functions that each produce their own error messages and need to return those messages to callers. But it falls apart when the functions call other functions that also produce error messages and those errors need to be forwarded or merged (#25722, #29700).

An inner Expected<InterruptResult, pair<ChainstateLoadError, bilingual_str>> could be transformed into an outer Expected<FooResult, pair<FooError, bilingual_str>> in that the outer error message could include some part of the inner error string. In Rust one could even preserve two levels of error enum values, that is not supported by this PR's util::Result nor util::Expected.

What I mean to say at the beginning of the thread is that there probably are other use-cases (definitely in #25722 and probably in #29700) which better motivate your modifications to util::Result in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #25665 (comment)

Thanks analogies with rust are interesting and I think the approach described is probably pretty good for error handling, but not well suited for error reporting, at least in high level application code. For manageable error handling you want to return coarse, rigidly structured types, and for good error reporting you want to be return more detailed less structured information. So the two goals are in tension and you will create problems (vague or incomplete error reports or overly complicated error handling) if you mix up the purposes of the returned information.

What I mean to say at the beginning of the thread is that there probably are other use-cases (definitely in #25722 and probably in #29700) which better motivate your modifications to util::Result in this PR.

Yes, your initial comment here led me to add some toy examples in the Result class documentation. I just don't see other ways to act on the feedback. The Result class has features for handling error messages and error status values, and the LoadChainstate example shows both of those features. It also has features for merging results that are useful when you have a library full of functions returning error messages. These are shown in #25722 and #29700 (which makes sense as separate PR's because they affect many more functions).

Copy link
Contributor Author

@ryanofsky ryanofsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reviews! The feedback's been very helpful and led to a few updates here.

Rebased f0aff63 -> 074cb32 (pr/bresult2.69 -> pr/bresult2.70, compare) due to conflict with #34006. Also:

  • Renamed methods and types to be consistent with the std::expected class.
  • Updated documentation to have more examples and focus on features not in the std::expected class.
  • Tried to respond to hodlinators concerns by moving message types and functions to a new util/messages.h file, to allow wider use and make it clear the result and message types are independent.

re: @arejula27 #25665 (comment)

If you agree this direction could be interesting, I can continue exploring the idea and evaluate how viable it is.

IIUC the suggestion there is to change implementation of ErrorString so it could work with std::expected and not be tied to util::Result. If so that does seem like a potentially useful change and maybe a simplifying one. TBH I don't know of code that would directly benefit from combining ErrorString with std::expected because I think validation and wallet code are better off using util::Result, which I've tried to show in s #25722 and #29700. But I don't see harm in generalizing the implementation.


re: hodlinator #25665 (review)

However, it seems the either/or aspect remains, if I'm not mistaken.
...
The benefit I see to using Expected even as part of "ResultLog" is that expected is a known pattern in C++, so it would slot into people's understanding. From there it comes down providing sufficient ergonomics of mutating/merging when using ErrorLog.

Thanks for looking into this. These experiments seem useful and likely to generate some good ideas. I will say I'm not really clear on what things you'd like to provide which the Result class isn't providing in this PR. I do think the current implementation should "slot into" someone's understanding of std::expected extremely well since it has the same interface, and just adds extra features for merging result instances and returning error messages.

using T = std::conditional_t<std::is_same_v<M, void>, std::monostate, M>;

std::variant<bilingual_str, T> m_variant;
//! tuple<variant<SuccessType, FailureType>, MessagesType>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #25665 (comment)

Isn't it closer to: tuple<optional<SuccessType>, unique_ptr<tuple<FailureType, MessagesType>>>

The current comment here says "Logically, a result object is equivalent to tuple<variant<SuccessType, ErrorType>, MessagesType>".

This is just trying to get across the idea that a Result holds a success value or a failure value, plus some messages.

IMO, adding unique_ptr to this description would make it more confusing (and the specific suggestion would also not be accurate since failure and message values can be set independently). Use of unique_ptr is an implementation detail not exposed to callers. The heap allocation is relevant since it could have performance implications, so it's mentioned in the comment. But it's not important for understanding what information is stored in the class or how the class can be used.

//! to void destination types is not allowed, since this would discard
//! source information.
template <bool DstConstructed, typename DstResult, typename SrcResult>
static void Move(DstResult& dst, SrcResult& src)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #25665 (comment)

Would anything be gained from making the template parameter an r-value? (Earlier commits do explicit std::move()s, justifying the function's name, but that justification appears to have dissipated).

There isn't a great way of forcing this to be an rvalue, since just replacing & && would make it a universal reference and make using the deduced SrcResult type more awkward. I also don't think it would be too helpful in any case, since this is not a public function and there are only 2 internal callers. Using a plain reference here is the simplest approach, I believe.

// 3. Path to a symlink to a directory.
// 4. For backwards compatibility, the name of a data file in -walletdir.
const fs::path wallet_path = fsbridge::AbsPathJoin(GetWalletDir(), fs::PathFromString(name));
fs::path wallet_path = fsbridge::AbsPathJoin(GetWalletDir(), fs::PathFromString(name));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #25665 (comment)

nit Q in 3c535e2 "wallet: fix clang-tidy warning performance-no-automatic-move": I got the impression our expectation is that all commits should pass CI. So I would expect this change to come before or in the same commit that would cause CI failure. Is that only valid for the HEAD commit when it comes to commits that resolve clang-tidy and similar checks?

I don't think there's a rule about this, but yes I wouldn't choose a commit only fixing a clang-tidy warning (that seems very brittle anyway) to be the first change here, since it seems easier to understand as a followup. Could reorder commits though if you or anyone else feels this is important.

Comment on lines +164 to +167
//! Container for SuccessType, providing public accessor methods similar to
//! std::optional methods to access the success value.
template <typename SuccessType, typename FailureType, typename MessagesType>
class SuccessHolder : public FailDataHolder<FailureType, MessagesType>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #25665 (comment)

I'm guessing the reason for the existence of a separate SuccessHolder type is in order to specialize away a minimal subset of functionality in SuccessHolder<void, ...>. If that's part of the reason, it could be admitted in the comment block for the main SuccessHolder template?

Yes exactly. Added a comment mentioning the void type to make this clearer.

@DrahtBot
Copy link
Contributor

🚧 At least one of the CI tasks failed.
Task lint: https://github.com/bitcoin/bitcoin/actions/runs/20166559939/job/57903572196
LLM reason (✨ experimental): Lint checks failed due to a new circular dependency: util/messages -> util/result -> util/messages.

Hints

Try to run the tests locally, according to the documentation. However, a CI failure may still
happen due to a number of reasons, for example:

  • Possibly due to a silent merge conflict (the changes in this pull request being
    incompatible with the current code in the target branch). If so, make sure to rebase on the latest
    commit of the target branch.

  • A sanitizer issue, which can only be found by compiling with the sanitizer and running the
    affected test.

  • An intermittent issue.

Leave a comment here, if you need help tracking down a confusing failure.

@arejula27
Copy link

arejula27 commented Dec 13, 2025

Re @ryanofsky

TBH I don't know of code that would directly benefit from combining ErrorString with std::expected because I think validation and wallet code are better off using util::Result

If you don't see any benefits keep your implementation, a generalisation can be implemented later if required 😄

Copy link
Contributor

@hodlinator hodlinator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates!

Annoyed at myself for not investigating my assumed either/or-ness of your proposed util::Result sooner. Somehow I'd interpreted FailDataHolders:

explicit operator bool() const { return !m_fail_data || !m_fail_data->failure; }

as plainly:

explicit operator bool() const { return !m_fail_data; }

The former makes it a clearer departure from util::Expected - warnings can be added and merged into parent results while still returning successful values.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit in ab94499 commit message:

  • Smaller type size section could mention that given values are for 64-bit platforms, since we still ship ARM32 releases.

Comment on lines +53 to +55
util::Result<kernel::InterruptResult, ChainstateLoadError> LoadChainstate(ChainstateManager& chainman, const kernel::CacheSizes& cache_sizes,
const ChainstateLoadOptions& options);
util::Result<kernel::InterruptResult, ChainstateLoadError> VerifyLoadedChainstate(ChainstateManager& chainman, const ChainstateLoadOptions& options);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I maintain that the current example in this PR of applying util::Result to LoadChainstate & VerifyLoadedChainstate is not well motivated as they don't use warnings or multiple errors, and are thus a better fit for util::Expected as in my linked commit.

It would be better to use a different example in this PR unless I'm still missing something.

{
protected:
struct FailData {
std::optional<std::conditional_t<std::is_same_v<ErrorType, void>, Monostate, ErrorType>> failure{};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Maybe this could be made more distinct from warnings by changing the name?

Suggested change
std::optional<std::conditional_t<std::is_same_v<ErrorType, void>, Monostate, ErrorType>> failure{};
std::optional<std::conditional_t<std::is_same_v<ErrorType, void>, Monostate, ErrorType>> error{};

Would hopefully decrease misunderstandings like I had myself in #25665 (review), since in that case it would be:

explicit operator bool() const { return !m_fail_data || !m_fail_data->error; }

@DrahtBot
Copy link
Contributor

DrahtBot commented Jan 7, 2026

🚧 At least one of the CI tasks failed.
Task iwyu: https://github.com/bitcoin/bitcoin/actions/runs/20769214790/job/59641680094
LLM reason (✨ experimental): IWYU detected include issues (failure generated from IWYU), causing the CI to fail.

Hints

Try to run the tests locally, according to the documentation. However, a CI failure may still
happen due to a number of reasons, for example:

  • Possibly due to a silent merge conflict (the changes in this pull request being
    incompatible with the current code in the target branch). If so, make sure to rebase on the latest
    commit of the target branch.

  • A sanitizer issue, which can only be found by compiling with the sanitizer and running the
    affected test.

  • An intermittent issue.

Leave a comment here, if you need help tracking down a confusing failure.

ryanofsky and others added 7 commits February 27, 2026 19:26
Add util::Result support for returning more error information. For better error
handling, adds the ability to return a value on failure, not just a value on
success. This is a key missing feature that made the result class not useful
for functions like LoadChainstate() which produce different errors that need to
be handled differently.

This change also makes some more minor improvements:

- Smaller type size. On 64-bit platforms, util::Result<int> is 16 bytes, and
  util::Result<void> is 8 bytes. Previously util::Result<int> and
  util::Result<void> were 72 bytes.

- More documentation and tests.
Previous commit causes the warning to be triggered, but the suboptimal return
from const statement was added earlier in 517e204.

Warning was causing CI failure https://cirrus-ci.com/task/6159914970644480
Add util::Result Update method to make it possible to change the value of an
existing Result object. This will be useful for functions that can return
multiple error and warning messages, and want to be able to change the result
value while keeping existing messages.
Add util::Result support for returning warning messages and multiple errors,
not just a single error string. This provides a way for functions to report
errors and warnings in a standard way, and simplifies interfaces.

The functionality is unit tested here, and put to use in followup PR
bitcoin#25722
Suggested by Martin Leitner-Ankerl <[email protected]>
bitcoin#25722 (comment)

Co-authored-by: Martin Leitner-Ankerl <[email protected]>
Avoid false positive maybe-uninitialized errors in
00_setup_env_native_fuzz_with_valgrind CI jobs. Similar errors also happen in
00_setup_env_arm and 00_setup_env_win64 jobs.

Problem was pointed out and fix was suggested by maflcko in
bitcoin#25665 (comment)

CI errors look like:
https://github.com/bitcoin/bitcoin/actions/runs/18881332233/job/53885646274?pr=25665

/home/admin/actions-runner/_work/_temp/src/wallet/test/util.cpp: In function ‘wallet::DescriptorScriptPubKeyMan* wallet::CreateDescriptor(CWallet&, const std::string&, bool)’:
/home/admin/actions-runner/_work/_temp/src/wallet/test/util.cpp:213:29: error: ‘*(const std::reference_wrapper<wallet::DescriptorScriptPubKeyMan>*)((char*)&spkm + offsetof(util::Result<std::reference_wrapper<wallet::DescriptorScriptPubKeyMan>, void, util::Messages>,util::Result<std::reference_wrapper<wallet::DescriptorScriptPubKeyMan>, void, util::Messages>::<unnamed>.util::detail::SuccessHolder<std::reference_wrapper<wallet::DescriptorScriptPubKeyMan>, void, util::Messages>::<unnamed>)).std::reference_wrapper<wallet::DescriptorScriptPubKeyMan>::_M_data’ may be used uninitialized [-Werror=maybe-uninitialized]
  213 |     return &spkm.value().get();
      |             ~~~~~~~~~~~~~~~~^~
/home/admin/actions-runner/_work/_temp/src/wallet/test/util.cpp:212:10: note: ‘*(const std::reference_wrapper<wallet::DescriptorScriptPubKeyMan>*)((char*)&spkm + offsetof(util::Result<std::reference_wrapper<wallet::DescriptorScriptPubKeyMan>, void, util::Messages>,util::Result<std::reference_wrapper<wallet::DescriptorScriptPubKeyMan>, void, util::Messages>::<unnamed>.util::detail::SuccessHolder<std::reference_wrapper<wallet::DescriptorScriptPubKeyMan>, void, util::Messages>::<unnamed>)).std::reference_wrapper<wallet::DescriptorScriptPubKeyMan>::_M_data’ was declared here
  212 |     auto spkm = Assert(keystore.AddWalletDescriptor(w_desc, keys,/*label=*/"", /*internal=*/false));
      |          ^~~~
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.