ORC-RT Error Handling Policy

Overview

ORC-RT uses a structured error handling system based on the orc_rt::Error and orc_rt::Expected<T> classes. This system provides type-safe error propagation that works consistently across different compilation configurations (with or without C++ exceptions).

Fundamental Principles

1. Error Representation

  • Success: Represented by Error::success() - a lightweight, zero-cost value
  • Failure: Represented by Error objects containing typed error information
  • Values with Potential Errors: Use Expected<T> to combine success values with error handling

2. Error Categories

Recoverable Errors: Environmental issues that can be handled gracefully

  • File I/O failures, network issues, malformed input
  • Use Error and Expected<T> return types
  • Examples: StringError, MyCustomError

Programmatic Errors: Violations of API contracts or program invariants

  • Use assertions
  • Should terminate the program immediately
  • Examples: Unexpected null pointers, invalid enum values

Important: Library Design Principles

ORC-RT is a library and must never call terminating functions like exit(), abort(), or std::terminate() in response to recoverable errors. Libraries should always return errors to their callers, allowing the application to decide how to handle them.

Core Error Types

Error

namespace orc_rt {
  class Error {
  public:
    // Create success value
    static Error success();

    // Check for failure
    explicit operator bool();  // true = failure, false = success

    // Type checking
    template<typename ErrT> bool isA() const;

    // Exception interop (when exceptions enabled)
    void throwOnFailure();
  };
}

Expected

template<typename T>
class Expected {
public:
  // Construction
  Expected(T Value);
  Expected(Error Err);

  // Check for success
  explicit operator bool();  // true = success, false = failure

  // Access value (success case)
  T& operator*();
  T* operator->();

  // Extract error (failure case)
  Error takeError();
};

Defining Custom Error Types

Use ErrorExtends<ThisT, ParentT>:

class CustomError : public ErrorExtends<CustomError, ErrorInfoBase> {
public:
  CustomError(std::string Message) : Message(std::move(Message)) {}

  std::string toString() const noexcept override {
    return "CustomError: " + Message;
  }

  const std::string& getMessage() const { return Message; }

private:
  std::string Message;
};

// Usage
Error doSomething() {
  if (/* error condition */)
    return make_error<CustomError>("Something went wrong");
  return Error::success();
}

Error Handling Patterns

Basic Error Propagation

Error processFile(StringRef Path) {
  if (auto Err = openFile(Path))
    return Err;  // Propagate error

  if (auto Err = validateFormat(Path))
    return Err;

  return Error::success();
}

Expected Usage

Expected<Data> loadData(StringRef Path) {
  auto FileOrErr = openFile(Path);
  if (auto Err = FileOrErr.takeError())
    return Err;

  return parseData(*FileOrErr);
}

// Alternative form
Expected<Data> loadData(StringRef Path) {
  if (auto FileOrErr = openFile(Path)) {
    auto& File = *FileOrErr;
    return parseData(File);
  } else {
    return FileOrErr.takeError();
  }
}

Error Consumption

Error values are most commonly passed up the stack (having interrupted whatever operation raised the error). Eventually errors must be consumed (failure to do so will trigger an assertion). Errors may be consumed using one of the following patterns:

// 1. Handle specific error types
handleAllErrors(mayFail(),
  [](const CustomError& CE) {
    // Handle CustomError
  },
  [](ErrorInfoBase& EIB) {
    // Handle any other error
  }
);

// 2. Report errors to the Session:
//    This should be done for Errors that cannot be passed further up the stack
//    (e.g. the have reached the root of some thread)
{
  if (auto Err = mayFail())
    S.reportError(std::move(Err));
  // thread ends here.
}

// 3. Convert to string and log:
//    This option may be used in contexts where a reference to the Session is
//    not available.
logError(toString(mayFail()));

// 4  Consume and ignore (explicit)
//    Errors can be explicitly consumed in cases where a failure is known to be
//    benign.
if (auto Err = tryPopulateFromOnDiskCache(...))
  consumeError(std::move(Err)); // Error indicates cache unavailable. Benign.

Exception Interoperability

When ORC_RT_ENABLE_EXCEPTIONS=On, ORC-RT provides bidirectional conversion between errors and exceptions.

Important: Exception Usage Policy

ORC-RT should not use exceptions internally. All ORC-RT functions should use Error and Expected<T> return types for error reporting. Exceptions should only be used at the boundaries:

  1. Converting external exceptions to errors when calling exception-throwing external code
  2. Converting errors to exceptions when returning from ORC-RT to exception-expecting client code

This policy ensures that:

  • ORC-RT works consistently whether exceptions are enabled or disabled
  • Error handling behavior is predictable and doesn't depend on exception propagation
  • The library remains compatible with codebases that disable exceptions (most LLVM projects)

Core Interop APIs

runCapturingExceptions: Converts exceptions to errors

// Return type depends on callback:
//   void        → Error
//   Error       → Error
//   Expected<T> → Expected<T>
//   T           → Expected<T>

auto Result = runCapturingExceptions([]() {
  return riskyOperation();  // might throw
});

Error::throwOnFailure: Converts errors to exceptions

try {
  auto Err = orcOperation();
  Err.throwOnFailure();  // Throws if Err represents failure
} catch (std::unique_ptr<StringError>& E) {
  // Catch specific error types
} catch (std::unique_ptr<ErrorInfoBase>& E) {
  // Catch any ORC error
} catch (...) {
  // Catch other exceptions
}

Exception Boundary Pattern

Use runCapturingExceptions to prevent exceptions from unwinding through ORC runtime:

Error safeCallback(std::function<void()> UserCallback) {
  return runCapturingExceptions([&]() {
    UserCallback();  // User code might throw
  });
}

ExceptionError Type

ExceptionError preserves C++ exceptions as Error values:

auto Err = runCapturingExceptions([]() {
  throw std::runtime_error("C++ exception");
});

// Err contains an ExceptionError wrapping the std::runtime_error
assert(Err.isA<ExceptionError>());

// Can be rethrown with original type preserved
Err.throwOnFailure();  // Rethrows std::runtime_error

Best Practices

1. Consistent Return Types

// Good: Consistent error handling
Expected<Data> loadData(StringRef Path);
Error saveData(const Data& D, StringRef Path);

// Bad: Mixed error handling
Data loadDataOrDie(StringRef Path);  // Inconsistent
bool saveData(const Data& D, StringRef Path, std::string* Error);  // C-style

2. Meaningful Error Messages

// Good: Descriptive, actionable
return make_error<StringError>(
  "Failed to parse config file '" + Path + "': invalid JSON at line " +
  std::to_string(LineNum)
);

// Bad: Vague
return make_error<StringError>("Parse error");

3. Appropriate Error Granularity

// Good: Specific error types enable targeted handling
class FileNotFoundError : public ErrorExtends<FileNotFoundError, ErrorInfoBase> {
  // ... specific to missing files
};

class PermissionError : public ErrorExtends<PermissionError, ErrorInfoBase> {
  // ... specific to permission issues
};

// Usage allows specific handling
handleAllErrors(openFile(Path),
  [](const FileNotFoundError& E) { /* try alternative locations */ },
  [](const PermissionError& E)   { /* request elevated access */ },
  [](ErrorInfoBase& E)           { /* generic fallback */ }
);

4. Exception Safety in Mixed Environments

// Safe pattern: Isolate exception-throwing code
Error integrateWithExceptionThrowingLibrary() {
  return runCapturingExceptions([&]() {
    externalLibrary.riskyOperation();
    return Error::success();
  });
}

// Unsafe: Exceptions can unwind through Error values
Error unsafeIntegration() {
  if (auto Err = orcOperation()) {
    log("Failed");  // might throw!
    return Err;     // ASSERTION FAILURE if log() throws
  }
  return Error::success();
}

5. Performance Considerations

  • Error::success() is zero-cost
  • Avoid creating error objects in hot paths when possible
  • Use early returns to minimize deep nesting
// Good: Early return, minimal overhead
Error fastPath(bool condition) {
  if (ORC_RT_LIKELY(condition))
    return Error::success();

  return make_error<StringError>("Rare error case");
}

Configuration Impact

Exception Disabled (ORC_RT_ENABLE_EXCEPTIONS=Off)

  • throwOnFailure() and runCapturingExceptions() are not available
  • ExceptionError is not available
  • All error handling uses Error/Expected<T> exclusively
  • Compatible with LLVM projects that disable exceptions

Exceptions Enabled (ORC_RT_ENABLE_EXCEPTIONS=On)

  • Full interoperability between errors and exceptions
  • Safe integration with exception-throwing external libraries
  • ExceptionError preserves exception values across Error boundaries
  • Compatible with standard C++ codebases using exceptions

Summary

ORC-RT's error handling system provides:

  • Type Safety: Errors have specific types that can be handled appropriately
  • Performance: Zero-cost success path, efficient error propagation
  • Flexibility: Works with or without C++ exceptions
  • Interoperability: Supports integration with exception-throwing code
  • Consistency: Uniform error handling across the entire codebase

By following these guidelines, ORC-RT maintains robust error handling that works across diverse integration environments while providing clear, actionable error information to users and developers.