fault is a lightweight crash reporting and panic library for C and C++, implemented in C++20. It provides a unified interface for alerting users, saving fault context and trace information when things go wrong, such as segmentation faults on Linux, unhandled SEH exceptions on Windows, std::terminate, or explicit panics and assertions.
When a C++ application crashes, the default behavior is often a silent exit or a cryptic "Segmentation Fault" message. fault changes this by intercepting system-level failures and providing developers with the context needed to debug them later, even from production contexts. It abstracts away the platform-specific complexities of POSIX signals and Windows Structured Exception Handling (SEH).
fault operates both at signal level and at c++ higher level, via its terminate handler and explicit panic modes. Whatever is the fault, fault performs common actions such as displaying a popup, terminal message, and writing a report, leaving both developers and potentially end users with some fatal context over silent and confusing crashes.
- Key Features
- Quick Start
- Features
- C-Language/Older C++ Support
- Utilities
- Headers
- Author's Note
- Third party components
- License
- Native C & C++ Support: Use the modern C++ API or the stable C-linkage interface for legacy projects.
- Unified Crash Handling: Intercepts SIGSEGV, SIGBUS, SIGILL, SIGFPE and SIGABRT on Linux, and main SEH Exception codes on Windows, such as EXCEPTION_STACK_OVERFLOW, EXCEPTION_ACCESS_VIOLATION, divisions by zero, illegal instructions or data misalignments.
- Async-Signal Safe (AS-Safe): Prioritizes safe "Object Trace" generation on restrictive environments, or has safeguards for user requested unsafe generation. See below for more info.
- C++ Terminate Override: Captures the stack trace of unhandled C++ exceptions before the runtime kills the process.
- User provided minimal context: For applications you'd like to distribute to others,
faultprovides users a fatal popup if a critical error occurs, instead of risking a silent and confusing crash. - Zero-Config Stack Traces: Powered by
cpptracefor high-quality, symbolicated traces. - Panic & Assert API: Provides
fault::panic(),fault::expect(),fault::expect_at(),fault::verify()andFAULT_ASSERTfor explicit, fail-fast error handling, including deferred evaluated on-failure actions. - Self-Contained: Can be bundled as a single static or shared library with no external runtime dependencies for the consumer.
- User configurability: Each fault action triggers report writing, user fatal Popups and summary message to terminal. User can switch these on/off independently for abnormal crashes, or user requested panic mode.
fault is designed for high-availability production environments where stability during a crash is non-negotiable.
- Async-Signal Safe (AS-Safe) Collection: During a fatal signal (Linux) or exception (Windows), the library avoids using the heap or complex C++ runtime calls as much as possible. It prioritizes collecting signal safe "Raw Object Trace", a collection of instruction pointers with memory offsets and binary paths, using the
cpptraceefforts as deriving mechanism. - Best-effort Safeguards If no safe trace can be collected, the user may optionally activate a best-effort approach to collect a regular trace. In this case, the library puts safeguards in place against deadlocks or recursive crashes, to ensure that the program is terminated cleanly, wether the (unsafe) trace is collected or not. Note: currently, on Windows, fully safe object traces can not be generated, and it is recommended for users to allow unsafe generation if a trace is desired. On Linux, safe traces can be collected only when using
libunwindwith_dl_find_object. By default,faultwill choose libunwind configuration parameter when fetchingcpptrace. Users may call fault::can_collect_safe_trace() to know wether a safe trace can be collected in restrictive environments. - Delayed Resolution: Instead of resolving symbols (function names/filenames) inside the crashed process,
faultoutputs a formatted "object trace" in its log. - Protected Debug Files: Developers can resolve these traces locally using their original
.debugor.pdbfiles. This means your production binaries can remain stripped (small and secure), while your logs remain fully actionable. - Trace Resolution is optional Traces can be optionally resolved for non-restrictive environments, if the user wishes. For safety, this is never done in Linux Posix or Windows SEH environments.
Add this to your CMakeLists.txt to integrate fault directly into your project:
include(FetchContent)
FetchContent_Declare(
fault
GIT_REPOSITORY [https://github.com/Ridrik/fault.git](https://github.com/Ridrik/fault.git)
GIT_TAG v0.1.3
)
FetchContent_MakeAvailable(fault)
# Link to your application
target_link_libraries(my_app PRIVATE fault::fault)By default, fault is fetched either as dynamic or static library, depending on ${BUILD_SHARED_LIBS} (On if not defined). Users may override it using FAULT_BUILD_SHARED=On/Off (boolean).
(Note: When building from source, cpptrace is fetched as part of it if FAULT_BUNDLE_CPPTRACE=On is selected (default), unless the target already exists. The same configurational options for cpptrace apply).
Note on State: If you link fault statically into multiple shared objects (DLLs/SOs) within the same process, each module will maintain its own independent configuration state. To share state across boundaries, build ``fault` as a shared library.
Initializing fault is done by a simple call, taking configuration parameters such as context names, report paths, and settings in what to execute/display in case of program abnormal behaviour.
#include <iostream>
#include <fault/fault.hpp> // Or #include <fault/core.hpp> if no <format> options wanted
void foo() {
volatile int* p{nullptr};
*p = 42;
}
int main() {
// Initialize global crash handlers (Signals, SEH, and Terminate)
if (!fault::init({.appName = "MyApp",
.buildID = "MyBuildID",
.crashDir = "crash",
.useUnsafeStacktraceOnSignalFallback = true,
.generateMiniDumpWindows = true})) {
std::cerr << "Failed to initialize fault.\n";
return EXIT_FAILURE;
}
foo();
return 0;
}Will output:
As well as a crash report, containing summaries, timing info and object traces (see below). On Windows, if set, it also generates a minidump to the same directory (.dmp file)
fault is resillient to edge cases where multiple threads concurrently perform abnormal operations
void foo() {
volatile int* p{nullptr};
*p = 42;
}
int main() {
// Initialize global crash handlers (Signals, SEH, and Terminate)
if (!fault::init({.appName = "MyApp",
.buildID = "MyBuildID",
.crashDir = "crash",
.useUnsafeStacktraceOnSignalFallback = true,
.resolveNonSignalTrace = true,
.generateMiniDumpWindows = true})) {
std::cerr << "Failed to initialize fault.\n";
return EXIT_FAILURE;
}
// Multi threading stress test - only one fault should register consistently
for (std::uint8_t i{0}; i < 6; ++i) {
std::thread([i] {
if (i == 0 || i == 2) {
foo();
}
if (i == 1 || i == 3) {
std::terminate();
}
if (i == 4) {
std::abort();
}
throw std::logic_error("Shouldn't have happened");
}).detach();
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
fault::panic("Some error");
return 0;
}Will produce consistent behaviour, only registering the 1st fault to enter any handler
fault also allows users to explicitly abort the program with similar actions and reports as the signal/termination handlers. Namely, the user may:
panicpanic may be called at any point to display terminal message, user popup, reports and dumps, before aborting the program.- FAULT_ASSERT fault assert is an assertion macro that checks for invariants, and panics if the assertion fails, displaying location information. By default, it only compiles in debug builds, but may be overriden by using
FAULT_ASSERTIONS=ON/OFF/DEFAULT(as strings) - fault::expect, fault::expect_at, FAULT_EXPECT, FAULT_EXPECT_AT. Similar to assertions, it performs invariant checks, panicking if failing. However, these are present also in release builds. fault::expect_at always displays location information (line, function, file), whereas, by default, fault::expect hides them on non-debug builds. Users may override
fault::expectlocation memory by usingFAULT_LOCATIONS=ON/OFF/DEFAULT(as strings) - fault::verify. Similar to the above, but it is present in any build type, and will never show location information.
Example:
int main() {
// Initialize global crash handlers (Signals, SEH, and Terminate)
if (!fault::init({.appName = "MyApp",
.buildID = "MyBuildID",
.crashDir = "crash",
.useUnsafeStacktraceOnSignalFallback = true,
.generateMiniDumpWindows = true})) {
std::cerr << "Failed to initialize fault.\n";
return EXIT_FAILURE;
}
Context context;
const auto result = add(5, 2);
// Assertion: compiles on debug builds by default, with source location
FAULT_ASSERT(result == 7, "Math is broken");
FAULT_ASSERT(result == 7, "Math is broken, expected {}, got {}", 7, result);
FAULT_ASSERT(result == 7, [&] {
doSomething();
const auto res = getSomeContext();
return std::format("math is broken. Context: {}", res.to_string()); });
// Expect: Always on, location information by default on debug builds
fault::expect(result == 7, "Math is broken");
// Each invariant check has a callable version for deferred evaluation
fault::expect(result == 7, [&] {
return std::format(
"This is a large formatted string on the heap that prints a complex context struct {}. This callable "
"provides deferred evaluation (only formats string on failure) and allows for follow up actions, if desired",
context.to_string())
});
FAULT_EXPECT(result == 7,
"Math is broken"); // expression 'result == 7' is also displayed & string is only evaluated in the cold path
// Or, with always source location
fault::expect_at(result == 7, "Math is broken");
// Always on, never with source location
fault::verify(result == 7, "Math is broken");
// verify, expect, expect_at and its macros (including FAULT_ASSERT) have overloads or versions for format strings
fault::verify(result == 7, "Math is broken. Result is {}", result);
fault::verify(result == 7, [&] { const auto res = getSomeContext(); return res.print(); });
FAULT_VERIFY(result == 7);
// Invariant failures on any of the above produces similar panic action
const auto c = add(5, 10);
FAULT_ASSERT(c != 15, "Bad math");
// overloads on macros available as well
FAULT_ASSERT(c != 15, "Bad math, got {}", c);
return 0;
}On debug build will abort with:
Note On Linux, if reraise signal is set, all these panic/assertions will end with reraising default SIGABRT, which usually prints the default abort message with core dumped (if system configured). On Windows, Minidump is instead explicitly generated if set on configuration, and afterwards the program is terminated. This follows the same final step as std::terminate handling.
Note All panic and assertions have overloads with invokable functions for deferred evaluation. In addition, there are also overloads or versions available for with formatted args, as long as the user includes fault/format.hpp or the general fault/fault.hpp. It is overloaded for all main expressions such as FAULT_ASSERT, fault::panic, fault::verify, fault::expect, fault::expect_at and their Macro versions.
fault::panic (or C's fault_panic) may be called explicitly by the user to perform a controlled program abort. It takes a user message string view (some overrides available). For instance, users may find it an useful feature after having caught a thrown exception in which the program needs to be aborted. fault makes it so that, whichever fault your program suffered, you get a saved trace report to resolve later, and your application users get a fatal popup instead of a silent crash. (Note that popups can be turned off in case the application is headless mode or when it must be restarted immediately)
void foo() {
throw std::runtime_error("Shouldn't have happened");
}
int main() {
// Initialize global crash handlers (Signals, SEH, and Terminate)
if (!fault::init({.appName = "MyApp",
.buildID = "MyBuildID",
.crashDir = "crash",
.useUnsafeStacktraceOnSignalFallback = true,
.generateMiniDumpWindows = true})) {
std::cerr << "Failed to initialize fault.\n";
return EXIT_FAILURE;
}
try {
foo();
} catch (const std::exception& e) {
fault::panic("Exception caught: {}", e.what());
}
}
fault's std::terminate handler, as well as its panic expressions, try to gather some information about the fault context, and can provide both hints and enhanced traces to the developer in its report log:
- Did you have a try/catch, but while something in your code threw, a destructor threw again, resulting in a call to std::terminate?
faultwill see that and alert the user. Plus, if it's withcpptracetry/catch, it will even propagate the trace to not only show the std::terminate/panic context, but also what the initial fault (throw) was that triggered the unwind event (see Example). - Did you save a trace to
fault(see Postponing traces and exceptions) but while you your threads were communicating or while your main thread was cleaning up, std::terminate orpanicwas triggered? It will also display as message and logs, including a dedicated propagated trace.
fault uses cpptrace internally to produce smooth cross-platform traces. This dependency is hidden by default to consumers, but if one wishes to use it, there are some additions that can be used with fault. Expanding from the explanation above, for instance, fault will provide automatic traces from exceptions on terminate handling, or override traces for panic. See the example below:
void terminateTest() {
cpptrace::try_catch(
[] {
struct LaunchThread {
LaunchThread() : t{[] { std::this_thread::sleep_for(std::chrono::seconds(1)); }} {}
std::thread t; // calls std::terminate if in a joinable state
} a;
bar(); // throws
a.t.join();
},
[](const std::exception& e) {
// Deal with it, recover or exit
});
}
int main() {
// Initialize global crash handlers (Signals, SEH, and Terminate)
if (!fault::init({.appName = "MyApp",
.buildID = "MyBuildID",
.crashDir = "crash",
.resolveNonSignalTrace = true})) {
std::cerr << "Failed to initialize fault.\n";
return EXIT_FAILURE;
}
terminateTest();
return 0;
}The user has a cpptrace::try_catch installed, and is explicitly joining the std::thread created. However, during execution, some function throws. Before reaching the catch, LaunchThread destructor runs, which sees std::thread in a joinable state and calls std::terminate. Normal object tracing would report the joinable thread as the fault, but not what triggered such sequence. By combining traces from exceptions in fault terminate handler, it'll also include the initial fault (in bar): (Note: An artifitial frame is put in the middle, labelled "====== UPSTREAM ======" for user visibility)
Another useful example, where one can panic with an explicit trace from exception:
#include <fault/fault.hpp>
#include <fault/adapter/stacktrace.hpp>
void foo() {
throw std::runtime_error("Shouldn't have happened");
}
int main() {
// Initialize global crash handlers (Signals, SEH, and Terminate)
if (!fault::init({.appName = "MyApp",
.buildID = "MyBuildID",
.crashDir = "crash",
.useUnsafeStacktraceOnSignalFallback = true,
.generateMiniDumpWindows = true})) {
std::cerr << "Failed to initialize fault.\n";
return EXIT_FAILURE;
}
// Override traces
cpptrace::try_catch([] { foo(); },
[](const std::exception& e) {
const auto cppObjectTrace =
cpptrace::raw_trace_from_current_exception().resolve_object_trace();
// needs #include "fault/adapter/stacktrace.hpp"
const auto objectTrace = fault::adapter::from_cpptrace(cppObjectTrace);
fault::panic(objectTrace, "Exception caught: {}", e.what());
});
}
fault provides the following utilities:
-
Shutdown requests: if set, it registers SIGINT and SIGTERM to set shutdown requests. This allows users to check, on their code, whenever a termination request has come by simply calling fault::has_shutdown_request (
fault_has_shutdown_requestfor C users). Users may also set themselves a shutdown request by calling fault::set_shutdown_request (fault_set_shutdown_requestfor C users), useful for multi-threaded applications. -
Save exceptions across threads. Have you detected abnormal behaviour in one of your threads in which the program needs to abort, but you'd prefer to have it be the main thread to abort so that you can perform needed minimal cleanup your app needs to save user's data?
faultmakes it so you can save a trace and message from a given thread, and signal to main for a shutdown request. Your main logic can then check if a saved trace exists and panic. Result? Your report will contain not only the main trace, but the trace to which your thread requested to be saved. Note You may also explicitly save a custom trace, if desired. If usingcpptrace::try_catch(or similar with its unwind interceptor),faultautomatically saves a trace from the current exception. Example:
// === Thread A ===
const auto t = std::thread([] {
try {
std::this_thread::sleep_for(std::chrono::seconds(1));
foo(); // throws
} catch (const std::exception& e) {
// Note: you may also pass a custom trace. If within cpptrace interceptor context,
// fault automatically saves a trace from exception
fault::save_traced_exception(std::format("Exception caught: {}", e.what()));
fault::set_shutdown_request();
}
});
// === Main thread ===
while (!fault::has_shutdown_request() && !myWindow.shouldClose) {
// Do main loop
}
// Loop exit, could have been normal termination (with your own logic), due to external request
// (SIGINT, SIGTERM), or due to thread exception:
if (fault::has_saved_traced_exception()) {
doCriticalCleanup();
fault::panic("Upstream exception"); // noreturn
}
doRegularCleanup();
// Or, more simply (if no side actions needed):
fault::panic_if_has_saved_exception("Upstream exception");
doRegularCleanup(); // Means no panic happened
- Symbol resolver script, which can be found in
scripts/symbol_resolver.py. It can resolve an object trace of the crash report given original .debug files in a subdirectory tree that can be mapped via the BUILD ID that the user gave tofaultconfiguration. Alternately, if the fault happened on the same machine as the script, it can take directly the object paths reported in it. Example:python scripts/symbol_resolver.py --use_same_paths=1 path/to/crash_report.log. Note it uses addr2line to resolve the trace. Feel free to customize it to your needs.
When pre-compiled, fault works for C consumers and C++ consumers on older standards, thanks to its fault.h API header. The behaviour largely mimics the one in C++.
void infinite_recursion() {
volatile char buffer[256];
infinite_recursion();
buffer[0] = 0;
}
int main() {
FaultConfig config = fault_get_default_config();
config.appName = "MyApp";
config.buildID = "MyBuildID";
config.crashDir = "crash";
config.useUnsafeStacktraceOnSignalFallback = true;
const FaultInitResult res =
fault_init(&config); // if no config changes wanted, user can call fault_init(NULL)
if (!res.success) {
printf("Failed to init fault\n");
return 1;
}
infinite_recursion(); // Triggers seg fault on linux & stack overflow on windows
// Example with callable for deferred evaluation (no source location, always on)
int status = 404;
fault_verify_c(status == 200, on_panic, &status);
printf("C API test passed\n");
return 0;
}fault reserves stack on both platforms to ensure stack overflows are properly displayed.
With crash report:
(...continues)
Other macros/functions available: FAULT_ASSERT (default for debug, always with source location), FAULT_EXPECT, FAULT_EXPECT_AT (always on, by default EXPECT has source location on debug builds only), FAULT_VERIFY. All these macros have a macro version with suffix "_C", standing for callbacks. Examples:
Note For C++ users that want callback options: while you can use these versions reliably, it is recommended to use the embedded overloads in fault::<function_name> (which invokes any callable).
const char* on_panic(void* data) {
int* val = (int*)data;
if (*val == 404) {
return "Resource not found";
}
return "Unknown system failure";
}
int main() {
FaultConfig config = fault_get_default_config();
config.appName = "MyApp";
config.buildID = "MyBuildID";
config.crashDir = "crash";
config.useUnsafeStacktraceOnSignalFallback = true;
const FaultInitResult res =
fault_init(&config); // if no config changes wanted, user can call fault_init(NULL)
if (!res.success) {
printf("Failed to init fault\n");
return 1;
}
int status = 404;
FAULT_VERIFY_C(status == 200, on_panic, &status);
FAULT_EXPECT_C(status == 200, on_panic, &status);
FAULT_EXPECT_AT_C(status == 200, on_panic, &status);
fault_verify_c(status == 200, on_panic, &status);
printf("C API test passed\n");
return 0;
}Fault uses a main, core, header, alongside optional ones that extend functionality (as well as dependencies). A short summary of the public headers is seen below:
| Header | Purpose | Notes |
|---|---|---|
| fault/core.hpp | core functionality, including initialization, panic & assertion, utilities | Suitable for most c++ consumers when not using format-based arguments or cpptrace adapter |
| fault/format.hpp | overloads or versions for panic and assertion functions & macros | Needed for std::format based strings |
| fault/fault.hpp | Everything accessible from fault C++ headers, minus the adapter |
Recommended to use if you don't mind the <format> header |
| fault/adapter/stacktrace.hpp | Simple header-only conversion between cpptrace and fault |
You'll only need this if you're using cpptrace directly and would like to provide a custom trace |
| fault/fault.h | Header for C consumers | If this library is pre-compiled, and you are using an older C++ version than C++20, then you may also use this |
The goal of this library is to provide safeguards that work reliably against all common software faults, without the need of complex tools or dependencies. fault achieves a smooth, thread-safe, default-async-signal-safe operations and, when needed (or by redundancy), safeguards against unsafe trace generation. Apart from this, fault also warns the user with basic fatal popups instead of silent crashes, which you may find useful wether when debugging or for end users.
Another goal of fault is to be non-intrusive in saving a trace. It does not try to resolve symbols by default, making it useful for production scenarios where, in case of a fault, the user/client can simply send the reports for you to resolve locally given your debug files.
Lastly, fault provides a modern framework for panic based commands and assertions, which is backed up by fault's overall termination handling. Users may find interesting as replacement for macros whenever applicable, as well as having invocable and format-based options, wether function-based or macro-based. Macros are also readily available for each version, and are still the choice for debug-only assertions.
fault uses cpptrace as driving mechanism to collect object traces smoothly across both platforms, and, whenever applicable, signal safe traces. See LICENSE_3RD_PARTY for the explicit component License.
| Component | Purpose | License |
|---|---|---|
| cpptrace | Lightweight stack trace and debugging helper | MIT** |
fault is licensed under the MIT License (see LICENSE file).
**fault depends on cpptrace.
- Standard Build: MIT.
- With libdwarf: If
cpptraceis configured to uselibdwarfand is linked statically, the resulting binary is subject to the LGPL license. Linking this condition statically tofaultwill therefore make the resulting binary LGPL.






