Dugan Chen's Homepage

Various Things

Go-Style Error Handing in Modern C++

There’s a lot of discussion these days about how to handle errors in C++ without using exceptions, or more specifically, the best currently-usable alternative to std::expected. Do you use a boost::outcome? An abseil::StatusOr? An std::variant? These are all ways to return a single value containing both a result and its error information, which may be “all okay”. This is the pattern used in Haskell and Rust.

I tried setting up a Go-style system where the result and its error information are returned as separate values. I think it’s pretty good. What follows is my contribution to the discussion.

This error-handling system extends the familiar idea of using an std::pair, where the first half contains the result and the second contains the error-status information. With std::tuple, structured binding and std::tie, you have C++’s answer to what Python calls unpacking and JavasScript calls destructuring.

Let me introduce an example of a partial libmpdclient wrapper to demonstrate this.

There were two technical considerations that went into the wrapper. One is that while libmpdclient uses error codes, those error codes aren’t necessarily exceptional. In some cases, you’re supposed to do a second query for more specific error information. Another is that when its functions return strings in the form of const char pointers, those strings become invalidated with subsequent API calls. Therefore, it makes sense to copy them immediately.

I chose not to wrap the API in a class; instead, I just replaced the C struct pointers with unique_ptrs. Also, I chose to return the error code and its error message as two separate values. It’s my bikeshed.

The main function connects to MPD and prints the list of albums, handling errors along the way. I think you’ll find that neither the library code nor the client code are unreasonably verbose.

A conventionally formatted version is here:

https://gist.github.com/duganchen/e40ff8a31d749f8b942e89022ba4271e

And this is the blog-formatted version, where the line lengths fit in the template:

#include <iostream>
#include <memory>
#include <mpd/client.h>
#include <string>
#include <tuple>
#include <vector>

namespace mpd {
using namespace std::string_literals;

typedef std::unique_ptr<mpd_connection,
                        decltype(&mpd_connection_free)>
    connection;

auto get_error(const connection &conn)
{
    mpd_error error_code{
        mpd_connection_get_error(conn.get())};

    if (MPD_ERROR_SUCCESS != error_code) {
        std::string message{
            mpd_connection_get_error_message(
                conn.get())};
        return std::make_tuple(error_code, message);
    }

    return std::make_tuple(error_code, ""s);
}

auto connection_new(const char *host,
                    unsigned port,
                    unsigned timeout_ms)
{
    auto ptr{
        mpd_connection_new(host, port, timeout_ms)};
    connection conn(ptr, &mpd_connection_free);

    if (!conn) {
        return std::make_tuple(std::move(conn),
                               MPD_ERROR_OOM,
                               ""s);
    }

    auto [error_code, error_message] = get_error(conn);
    return std::make_tuple(std::move(conn),
                           error_code,
                           error_message);
}

auto search_db_tags(const connection &conn,
                    mpd_tag_type tag_type)
{
    if (mpd_search_db_tags(conn.get(), tag_type)) {
        return std::make_tuple(MPD_ERROR_SUCCESS, ""s);
    }

    return get_error(conn);
}

auto search_commit(const connection &conn)
{
    if (mpd_search_commit(conn.get())) {
        return std::make_tuple(MPD_ERROR_SUCCESS, ""s);
    }

    return get_error(conn);
}

auto recv_tags(const connection &conn,
               mpd_tag_type tag_type)
{
    std::vector<std::string> tags;

    mpd_pair *pair{};
    while ((pair = mpd_recv_pair_tag(conn.get(),
                                     tag_type))
           != nullptr) {
        tags.emplace_back(pair->value);
        mpd_return_pair(conn.get(), pair);
    }

    auto [error_code, message] = get_error(conn);
    return std::make_tuple(std::move(tags),
                           error_code,
                           message);
}
}; // namespace mpd

int main()
{
    auto [connection, error_code, error_message]
        = mpd::connection_new("localhost", 6600, 0);

    if (MPD_ERROR_SUCCESS != error_code) {
        std::cout << error_message << "\n";
        return 1;
    }

    std::tie(error_code, error_message)
        = mpd::search_db_tags(connection,
                              MPD_TAG_ALBUM);
    if (MPD_ERROR_SUCCESS != error_code) {
        std::cout << error_message << "\n";
        return 1;
    }

    std::tie(error_code, error_message)
        = mpd::search_commit(connection);
    if (MPD_ERROR_SUCCESS != error_code) {
        std::cout << error_message << "\n";
        return 1;
    }

    std::vector<std::string> tags;
    std::tie(tags, error_code, error_message)
        = mpd::recv_tags(connection, MPD_TAG_ALBUM);

    if (MPD_ERROR_SUCCESS != error_code) {
        std::cout << error_message << "\n";
        return 1;
    }

    for (auto tag : tags) {
        std::cout << tag << "\n";
    }

    return 0;
}