Library / SDK
lamarrr/STX avatar
lamarrr/STX

STX: a C++17 and C++20 toolbox for error handling without exceptions

C++17 & C++ 20 error-handling and utility extensions.

630 stars21 forksC++MIT

At a glance

What is it?
STX is a header collection of C++ utilities built around Result and Option with monadic methods, plus Span, Vec, Rc, a backtrace library and panic hooks. It targets embedded, real-time and safety-critical work, where exception-free, RTTI-free and allocation-free code is a requirement rather than a preference.
Who is it for?
STX suits a C++ codebase that cannot use exceptions, either because it targets an embedded or real-time platform or because it wants error paths to be visible at every call site, and the TRY_OK macro plus the consuming semantics of the monadic methods are the parts to study.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 116 days ago.
What is it written in?
Mainly C++, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Result and Option, with a match that handles both arms at once

STX is a collection of libraries and utilities designed to make working with C++ easier and less error-prone, and its centre of gravity is a pair of error-handling types.

Option is the optional-value type. The example is a division that cannot succeed:

cpp
#include <iostream>

#include "stx/option.h"

using stx::Option, stx::Some, stx::None;

auto safe_divide(double numerator, double denominator) -> Option<double> {
  if (denominator == 0.0) return None;
  return Some(numerator / denominator);
}

int main() {
  safe_divide(5.0, 2.0).match(
      [](auto value) { std::cout << "Result: " << value << std::endl; },
      []() { std::cout << "Cannot divide by zero" << std::endl; });
}

The match takes two lambdas, one for the value and one for the absence. There is no early return, no sentinel value and no exception, and the compiler will not let you forget the absent case because the call requires both.

Result is the same shape with an error type. The example parses a version byte out of a header:

cpp
#include <array>
#include <cinttypes>
#include <iostream>

#include "stx/result.h"

using std::array, std::string_view;
using namespace std::literals;

using stx::Result, stx::Ok, stx::Err;

enum class Version { V1 = 1, V2 = 2 };

auto parse_version(array<uint8_t, 6> const& header) -> Result<Version, string_view> {
  switch (header.at(0)) {
    case 1:
      return Ok(Version::V1);
    case 2:
      return Ok(Version::V2);
    default:
      return Err("Unknown Version"sv);
  }
}

int main() {
  parse_version({2, 3, 4, 5, 6, 7}).match([](auto version){
    std::cout << "Version: " << static_cast<int>(version) << std::endl;
  }, [](auto error){
    std::cout << error  << std::endl;
  });

}

The error is a string_view, and the Ok and Err constructors keep the two cases visually distinct at every return point. The result type is a forwarding type: it never makes implicit copies of what it holds, because its purpose is to be a return channel where duplication is exactly what you do not want.

TRY_OK propagates the error and hands you the value

The macro that makes the pattern ergonomic is TRY_OK, and it works in the direction most people expect from a language with exceptions.

TRY_OK assigns the successful value to its first parameter if the call returned an Ok, and otherwise propagates the error value. Applied to the version parser:

cpp
// As in the example above
auto parse_version(array<uint8_t, 6> const& header) -> Result<Version, string_view>;

auto parse_data(array<uint8_t, 6> const& header) -> Result<uint8_t, string_view> {
  TRY_OK(version, parse_version(header));
  return Ok(version + header[1] + header[2]);
}

int main() {
  auto parsed = parse_data({2, 3, 4, 5, 6, 7}).unwrap();

  std::cout << parsed << std::endl;
}

The function returns Result, and the failure of parse_version becomes the failure of parse_data without a single conditional in between. That is the whole argument for the library: the error path is written once, at the point where the error is created, and the happy path stays readable.

The macro also accepts const and volatile qualifiers on the assigned value, so the same call can bind a constant:

cpp
auto parse_data(array<uint8_t, 6> const& header) -> Result<uint8_t, string_view> {
  TRY_OK(const version, parse_version(header));
  return Ok(version + header[1] + header[2]);
}

What you do not get is a stack trace or a message attached to the error automatically, since the error value is whatever type you chose, here a string_view. If you want context on the failure, you have to build it into the error type yourself.

Consuming semantics: unwrap on a temporary compiles, on a named variable it does not

This is the rule in the guidelines that surprises people most, and it is worth reading twice before you adopt the library.

Methods like map, unwrap, or_else and most of the monadic methods consume the stored value, and therefore the Result or Option has to be destroyed since its lifetime has ended. So this compiles:

cpp
float result = safe_divide(n, d).unwrap(); // compiles, because 'safe_divide' returns a temporary

And this does not:

cpp
Option option = safe_divide(n, d);
float result = option.unwrap();  // will not compile, because 'unwrap' consumes the value and is only usable with temporaries (as above) or r-value references (as below)

The reason is that unwrap would leave the named Option in a moved-from state, and the library refuses to let that happen silently. If the value is no longer needed, you take an r-value reference:

cpp
Option option = safe_divide(n, d);
float result  = std::move(option).unwrap(); // will compile, the value is moved out of 'option' , 'option' should not be used any more

And the note is the standard one for moved-from objects: Option and Result are not to be used after a std::move, since the objects will be left in an unspecified state.

This is a defensible design rather than an annoyance, because it is the same rule that stops you reading a moved-from std::string. The cost is that code which wants to inspect a Result twice has to be written differently, which is why the next guideline matters.

Since Result and Option do not make implicit copies of the contained object, an explicit copy has to be asked for. clone makes a copy of the whole Option, and value returns an l-value reference that is then copied by the caller:

cpp
Option option = safe_divide(n, d);
float result = option.clone().unwrap(); // note that 'clone()' explicitly makes a copy of the 'Option'
cpp
Option option = safe_divide(n, d);
float result = option.value(); // note that 'value()' returns an l-value reference and 'result' is copied from 'option''s value in the process

And value on a temporary does not compile, because it always returns an l-value reference, so unwrap is the one to use there.

The rest of the library, and the constraints it is built for

Beyond the two error types, the feature list is a catalogue of things a systems programmer ends up writing for themselves.

There is an experimental async library, runtime and scheduler. There is a C-API-compatible functor object, Fn<Return(Args...)>, which is how you get a callable across a C boundary. There are memory allocators, resource reference counting through Rc<Handle>, and a Span<T> with helper methods that eliminates the use of iterator pairs. Vec<T> is offered with trivial relocation, described as faster than std::vector<T>.

The failure side has as much attention as the success side. Fail-fast behaviour is called abandonment and is reached through panic functions, there are runtime panic hooks, a backtrace library, source location, and panic with backtraces. There is also a UTF-8 string iterator method.

The constraints are stated as a list of properties, and they are the reason to look at this library at all. It is exception-free, RTTI-free and memory allocation free, with the no-std qualifier. Error handling is space and time deterministic, and value lifetimes are deterministic, which are two things exceptions cannot offer. It is described as portable and suitable for embedded systems, real-time systems, safety-critical systems and operating systems. The library eliminates repetitive and abstractable error-handling code via monadic extensions, and it is described as easy to use and hard to misuse.

One point in the guidelines is easy to miss and costs real time if you do not know it. Result and Option work in a constexpr context, that is, compile-time error handling, in C++ 20 only, and you can check whether your build supports it by looking at whether the macros STX_RESULT_IS_CONSTEXPR and STX_OPTION_IS_CONSTEXPR are set to 1, with a constexpr_test file in the tests directory as the reference.

The recommendation about warning levels is the other practical one: to ensure you never forget to use the returned errors and results, raise the warning levels for your project, with -Wall, -Wextra and -Wpedantic on GNUC-based compilers and /W4 on MSVC. The library works best when the compiler is helping you.

Building it, testing it, and the release history gap

The build requirements are a short list: CMake, Make or Ninja, a C++ 17 or C++ 20 compiler, Git, and Doxygen with Graphviz for the documentation.

The repository layout shows a project with a real test and example story. The include/ directory holds the headers, since the library is header-based, with src/ for anything compiled, tests/ for the test suite, examples/ with its own CMakeLists.txt and a README, plus extension/, cmake-modules/, scripts/, docs/ and assets/. The example files listed are option.cc, result.cc, panic.cc, panic_backtrace.cc and panic_hook.cc, which matches the feature list, one example per headline capability rather than a single kitchen-sink program.

The CI badges tell you which toolchains are exercised. There are workflows for C++ 17 with clang 14 on macOS 12, clang 14 on Ubuntu 22.04, gcc 12 on Ubuntu 22.04, and MSVC 2019 on Windows, plus a clang 15 build on Ubuntu 22.04 under qemu-arm. That last one is the interesting badge, because an ARM run under emulation is how you check that a systems library does not quietly assume x86.

The release history is where the caveats are. The recent releases are v1.0.3 on 2023-06-04, v1.0.4 on 2023-07-03 and v1.0.5 on 2023-11-20, and the last push to the repository was on 2026-06-29. So the newest tag is about three years old while the working tree has moved more recently, and there are no releases newer than v1.0.5 to point at. For a header-only library that is less constraining than for a binary, but it does mean you are consuming a version that has not been tagged in a long time, and the documentation on the project page at lamarrr.github.io/STX is the place to check what the current state actually is.

The licence is MIT, with a LICENSE file at the repository root and a CODE_OF_CONDUCT.md beside it.

Editorial conclusion

STX suits a C++ codebase that cannot use exceptions, either because it targets an embedded or real-time platform or because it wants error paths to be visible at every call site, and the TRY_OK macro plus the consuming semantics of the monadic methods are the parts to study. It does not suit a team that already has a settled error handling convention, since adopting it means committing to the consuming and forwarding semantics, and the result of unwrap on a temporary works while the same call on a named variable does not compile. Before you adopt it, check the release history, since the newest tag is v1.0.5 from 2023-11-20 and the last push was on 2026-06-29, and read the guidelines on raising warning levels, which is what makes the compiler catch an ignored error.

Frequently asked questions

What is STX in C++?

It is a collection of C++17 and C++20 utility libraries, with an experimental async library, runtime and scheduler, a C-API-compatible Fn type, memory allocators, Rc reference counting, Span, Vec, Result and Option with monadic methods, panic hooks and a backtrace library. It is exception-free, RTTI-free and allocation free, and works with no-std.

How does the TRY_OK macro work in STX?

TRY_OK assigns the successful value to its first parameter if the call returned an Ok, and otherwise propagates the error value. It also accepts const and volatile qualifiers on the assigned value, so you can write TRY_OK(const version, parse_version(header)).

Why does calling unwrap on a named Option or Result not compile in STX?

Because the monadic methods including unwrap consume the stored value, and the value has to be destroyed once consumed. You can call unwrap on a temporary, or obtain an r-value reference with std::move, and after a move the object must not be used again.

Can STX Result and Option be used in a constexpr context?

In C++ 20 they can be used in a constexpr context for compile-time error handling. To check whether your build supports it, look at whether the macros STX_RESULT_IS_CONSTEXPR and STX_OPTION_IS_CONSTEXPR are set to 1, with the constexpr_test file in the tests directory as a reference.

Official sources

  1. lamarrr/STX on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/lamarrr-stx.svg)](https://hysenlabs.com/projects/lamarrr-stx)

Was this useful?