Throwing and Handling Exceptions

This page shows how to use Erbsland Core exceptions in application code. You will learn where to catch an error, which text interface to use, how to retain an original failure as a cause, and how Application turns an uncaught library exception into a consistent error report.

Catch Errors Where You Can Act

Throw an exception when a function cannot produce the result promised by its interface. Catch it where the program can make a meaningful decision: retry an operation, ask for corrected input, select a fallback, or translate the failure into a higher-level error.

Order handlers from the most specific type to the broadest type. A local parser can recover from ParseError because it understands the input. A subsystem boundary may instead handle every RuntimeError in the same way.

/// A parser throws `ParseError` when its input violates the notation grammar.
auto parseDynamics(const el::String &text) -> el::String {
    if (text == "piano"_el || text == "forte"_el) {
        return text;
    }
    throw el::ParseError{"The dynamic marking must be 'piano' or 'forte'."_el, el::CpIndex{0}};
}

/// Catch the most specific recoverable exception close to the operation that understands it.
/// Broader runtime-error handlers belong at subsystem or application boundaries, where a generic fallback is useful.
void throwAndCatch() {
    try {
        el::io::printLine("Dynamics: "_el, parseDynamics("fortissimo"_el));
    } catch (const el::ParseError &error) {
        el::io::printLine("Please correct the notation: "_el, error.toString());
    } catch (const el::RuntimeError &error) {
        el::io::printLine("The score could not be loaded: "_el, error.reason());
    }
}
Please correct the notation: The dynamic marking must be 'piano' or 'forte'. at code point
 0

Do not routinely catch LogicError to continue execution. It identifies faulty program logic, such as an invalid parameter or an operation outside a supported API range. Catching it only to suppress the failure can leave the program in a state its author did not design.

Choose the Right Text Interface

Exception provides several views of its information:

  • Exception::reason() returns the reason supplied when the exception was created. Use it when adding context or making a programmatic decision that still needs human-readable text.

  • Exception::toString() returns a compact description and may add exception-specific details, such as a parse position or parameter name.

  • Exception::what() provides the null-terminated string required by std::exception. It intentionally remains a compatibility interface and returns the reason.

  • Exception::diagnostic() creates the structured diagnostic for this exception. It does not include the exception’s cause chain.

/// An exception exposes text for different consumers.
/// `reason()` is the stable reason supplied by the thrower, `toString()` may add exception-specific details, and
/// `what()` provides the null-terminated compatibility string expected by standard C++ interfaces.
void exceptionText() {
    try {
        const auto tempo = el::String{"速い"_el}.toIntegerOrThrow<int>();
        el::io::printLine("Tempo: "_el, tempo);
    } catch (const el::ParseError &error) {
        el::io::printLine("reason(): "_el, error.reason());
        el::io::printLine("toString(): "_el, error.toString());
        el::io::printLine("what(): "_el, error.what());
    }
}
reason(): Expected an integer number, got no digits
toString(): Expected an integer number, got no digits at code point 0
what(): Expected an integer number, got no digits

Prefer structured diagnostics for final user-facing reports. Concatenating toString() values loses semantic fields, source locations, rendering styles, and the relationship between causes.

Preserve the Original Cause

Lower layers often expose technical details that are not meaningful to the caller. Translate such a failure into an exception from your own domain, but preserve the active exception with std::current_exception(). The outer reason then explains which application operation failed, while Exception::cause() retains the original evidence.

/// Preserve a caught failure with `std::current_exception()` when translating it into a domain-level exception.
/// The outer exception explains the failed operation, while `cause()` retains the original technical failure.
void causeChains() {
    try {
        try {
            throw std::runtime_error{"audio device disconnected"};
        } catch (...) {
            throw el::RuntimeError{"Shakuhachi recording could not be started."_el, std::current_exception()};
        }
    } catch (const el::RuntimeError &error) {
        el::io::printLine("Outer reason: "_el, error.reason());
        el::io::printLine("Has cause: "_el, error.hasCause());
        try {
            std::rethrow_exception(error.cause());
        } catch (const std::exception &cause) {
            el::io::printLine("Original cause: "_el, cause.what());
        }
    }
}
Outer reason: Shakuhachi recording could not be started.
Has cause: true
Original cause: audio device disconnected

The cause is a standard std::exception_ptr. It may therefore refer to another Erbsland Core exception, a foreign std::exception, or an unknown exception. DiagnosticHelper understands all three cases and limits pathological cause depth while building a complete report.

Let Application Report the Final Error

Application::run() is the normal reporting boundary for an Erbsland application. It catches Exception, calls cleanup(), builds a diagnostic document including causes, and renders it through the application’s system output. A callback exception from the automatically managed main event loop stops that loop, shuts down managed event threads, and then reaches this same boundary. A foreign std::exception is not caught directly by run(), but it is rendered when preserved as the cause of an Erbsland Core exception.

Use ApplicationError when application code wants to choose the final exit code or provide a title, description, source, and code location. Build those optional details in an ApplicationErrorContext.

/// Throw `ApplicationError` from code executed by `Application::run()` to request consistent error reporting.
/// Its context provides a title, description, source information, location, and the process exit code.
void applicationReporting() {
    auto context = el::ApplicationErrorContext{
        "The score could not be read."_el,
        "The main melody contains an unrecognized symbol."_el,
        el::ExitCode::failure(),
    };
    context.setSourceName("春の合奏"_el)
        .setSourcePath("scores/春の合奏.music"_el)
        .setCodeLocation(el::CodeLocation{el::LineIndex{11}, el::ColumnIndex{8}});
    throw el::ApplicationError{std::move(context), {}};
}
Error: The score could not be read.
  The main melody contains an unrecognized symbol.
Error Source:
  Source: 春の合奏
  Path:   scores/春の合奏.music
  Line:   12
  Column: 9

Use ApplicationError for a failure that should end the current application run with an intentional report. For a recoverable library or domain operation, keep the more specific runtime exception and let the caller decide whether it can continue.

The next page, Working with Diagnostic Data, explains how to build the same reports manually when you are not using Application::run() or need a different reporting boundary.