Skip to main content
Draconis++ uses Result<T, E> types for explicit, exception-free error handling inspired by Rust.

Result type

The Result<T, E> type represents either a success value of type T or an error value of type E:
template <typename Tp = Unit, typename Er = error::DracError>
using Result = std::expected<Tp, Er>;

template <typename Er = error::DracError>
using Err = std::unexpected<Er>;

using Unit = void;  // For Result<void>
Result<T> defaults to using DracError as the error type, so you can write Result<String> instead of Result<String, DracError>.

DracError structure

The library uses a structured error type with context:
struct DracError {
  String message;               // Descriptive error message
  std::source_location location; // Where the error occurred
  DracErrorCode code;           // Error category
};

Error codes

enum class DracErrorCode : u8 {
  ApiUnavailable,     // OS service/API unavailable
  ConfigurationError, // Configuration issue
  CorruptedData,      // Data corrupt or inconsistent
  InternalError,      // Application logic error
  InvalidArgument,    // Invalid function argument
  IoError,            // Filesystem/pipe error
  NetworkError,       // DNS/connection failure
  NotFound,           // Resource not found
  NotSupported,       // Operation not supported
  Other,              // Unclassified error
  OutOfMemory,        // Memory exhaustion
  ParseError,         // Failed to parse data
  PermissionDenied,   // Insufficient permissions
  PermissionRequired, // Elevated privileges needed
  PlatformSpecific,   // Platform-specific error
  ResourceExhausted,  // System resource limit
  Timeout,            // Operation timed out
  UnavailableFeature, // Hardware/OS feature absent
};

Returning errors

Use the ERR macro to create and return errors:
auto readFile(StringView path) -> Result<String> {
  std::ifstream file(path);
  if (!file)
    ERR(DracErrorCode::IoError, "Failed to open file");
  
  String content((std::istreambuf_iterator<char>(file)), {});
  return content;
}

Error macros

// Basic error with code and message
ERR(DracErrorCode::NotFound, "User not found");

// Formatted error message
ERR_FMT(DracErrorCode::InvalidArgument, "Invalid ID: {}", userId);

// Forward an existing error
ERR_FROM(existingError);
The ERR macro automatically captures the source location (file, line, function) for better debugging.

Handling results

Checking and unwrapping

auto result = readFile("config.toml");

if (result) {
  // Success: access value
  String content = *result;
  std::println("Content: {}", content);
} else {
  // Error: access error
  auto& error = result.error();
  std::println("Error: {}", error.message);
  std::println("Code: {}", static_cast<u8>(error.code));
}

Value extraction methods

// Check if result contains a value
if (result.has_value()) { /* ... */ }

// Access value (throws if error)
String value = result.value();

// Access value with operator*
String value = *result;

// Access error (undefined if success)
auto error = result.error();

// Provide default value on error
String value = result.value_or("default");
Accessing the value with *result or .value() when the Result contains an error is undefined behavior. Always check first with if (result) or .has_value().

Error propagation

The TRY macro provides Rust-style error propagation:
auto processFile(StringView path) -> Result<Data> {
  // If readFile returns an error, immediately return that error
  String content = TRY(readFile(path));
  
  // If parseJson returns an error, immediately return that error
  Data data = TRY(parseJson(content));
  
  return data;
}
This is equivalent to:
auto processFile(StringView path) -> Result<Data> {
  auto contentResult = readFile(path);
  if (!contentResult)
    return Err(contentResult.error());
  String content = *contentResult;
  
  auto dataResult = parseJson(content);
  if (!dataResult)
    return Err(dataResult.error());
  Data data = *dataResult;
  
  return data;
}

TRY_VOID for Result with no value

For functions returning Result<> (no value):
auto initializeSystem() -> Result<> {
  TRY_VOID(validateConfig());
  TRY_VOID(initializeCache());
  TRY_VOID(connectToDatabase());
  return {};
}
On MSVC, you may need to use TRY_RESULT(var, expr) instead of auto var = TRY(expr) due to compiler limitations.

Platform-specific error handling

Windows

auto GetMemInfo() -> Result<ResourceUsage> {
  MEMORYSTATUSEX memStatus;
  memStatus.dwLength = sizeof(memStatus);
  
  if (!GlobalMemoryStatusEx(&memStatus))
    ERR(DracErrorCode::ApiUnavailable, "GlobalMemoryStatusEx failed");
  
  return ResourceUsage{
    .usedBytes = memStatus.ullTotalPhys - memStatus.ullAvailPhys,
    .totalBytes = memStatus.ullTotalPhys
  };
}

Linux

auto GetOSVersion() -> Result<String> {
  std::ifstream file("/etc/os-release");
  if (!file)
    ERR(DracErrorCode::NotFound, "/etc/os-release not found");
  
  String line;
  while (std::getline(file, line)) {
    if (line.starts_with("PRETTY_NAME=")) {
      return line.substr(13, line.length() - 14); // Remove quotes
    }
  }
  
  ERR(DracErrorCode::ParseError, "PRETTY_NAME not found in os-release");
}

macOS

auto GetCpuName() -> Result<String> {
  char buffer[256];
  size_t size = sizeof(buffer);
  
  if (sysctlbyname("machdep.cpu.brand_string", buffer, &size, nullptr, 0) != 0)
    ERR(DracErrorCode::ApiUnavailable, "sysctlbyname failed");
  
  return String(buffer);
}

Best practices

1

Use Result<T> for operations that can fail

// Good: Explicit failure mode
auto parseConfig(StringView path) -> Result<Config>;

// Avoid: Hidden exceptions
auto parseConfig(StringView path) -> Config; // throws
2

Use specific error codes

// Good: Specific error code
ERR(DracErrorCode::NotFound, "Config file not found");

// Avoid: Generic error
ERR(DracErrorCode::Other, "Something went wrong");
3

Provide context in error messages

// Good: Context included
ERR_FMT(DracErrorCode::InvalidArgument, 
        "Invalid user ID: {} (must be positive)", userId);

// Avoid: Vague message
ERR(DracErrorCode::InvalidArgument, "Invalid argument");
4

Use TRY for error propagation

// Good: Clean propagation
String content = TRY(readFile(path));
Data data = TRY(parseJson(content));

// Avoid: Manual checking
auto result1 = readFile(path);
if (!result1) return Err(result1.error());
auto result2 = parseJson(*result1);
if (!result2) return Err(result2.error());
5

Chain operations with TRY

auto process() -> Result<Output> {
  auto input = TRY(fetchInput());
  auto validated = TRY(validate(input));
  auto transformed = TRY(transform(validated));
  return Output{transformed};
}

Error handling vs exceptions

AspectResult<T>Exceptions
VisibilityExplicit in signatureHidden
PerformanceZero-cost on successStack unwinding overhead
Error propagationExplicit with TRYImplicit with throw
Type safetyCompile-time checkedRuntime checked
Control flowExplicitNon-local
Draconis++ reserves exceptions for truly exceptional situations (e.g., programmer errors, fatal failures). Use Result<T> for expected error conditions like file I/O, parsing, or network operations.

Example: complete error handling flow

using namespace draconis::utils::types;
using namespace draconis::utils::error;

auto readConfig(StringView path) -> Result<String> {
  std::ifstream file(path);
  if (!file)
    ERR(DracErrorCode::IoError, "Failed to open config file");
  
  String content((std::istreambuf_iterator<char>(file)), {});
  return content;
}

auto parseConfig(StringView content) -> Result<Config> {
  try {
    return toml::parse_str(content);
  } catch (const toml::syntax_error& e) {
    ERR_FMT(DracErrorCode::ParseError, "TOML parse error: {}", e.what());
  }
}

auto loadConfig(StringView path) -> Result<Config> {
  String content = TRY(readConfig(path));
  Config config = TRY(parseConfig(content));
  return config;
}

int main() {
  auto config = loadConfig("config.toml");
  
  if (config) {
    std::println("Config loaded successfully");
    // Use config
  } else {
    auto& err = config.error();
    std::println("Failed to load config: {}", err.message);
    std::println("Location: {}:{}", err.location.file_name(), 
                                     err.location.line());
    return 1;
  }
  
  return 0;
}
Error definitions are in /home/daytona/workspace/source/include/Drac++/Utils/Error.hpp:14.

Build docs developers (and LLMs) love