Building Event-Driven Applications
An event-driven application begins work and then reacts to completions, timers, input, or service notifications.
The default Application::main() supplies the main event loop, so the
derived class only needs to arrange the first event and call quit() when the process should finish.
Entering the Main Event Loop
The following application schedules its first task during initialization.
After command-line parsing succeeds, the unchanged base main() enters the event loop and dispatches the queued work.
The second callback calls quit(), which records the exit code and ends
managed event processing in an orderly way.
core/EventApplication/main.cpp
/// Derive from `Application` when the program is driven by asynchronous work on the main event loop.
///
/// The default `Application::main()` enters the event loop.
/// Calling `quit()` from an event callback finishes the loop and returns the requested exit code.
class MaintenanceApplication final : public el::Application {
public:
using Application::Application;
protected: // implement Application
void initialize() override {
info().setApplicationName("Maintenance Queue"_el);
info().setApplicationVersion(el::Version{1, 0, 0});
events()->invoke([this]() -> void { runMaintenance(); });
}
private:
void runMaintenance() {
el::io::printLine("maintenance job started"_el);
events()->invoke([this]() -> void {
el::io::printLine("maintenance job completed"_el);
quit();
});
}
};
/// Create the application and leave event dispatch to its default main implementation.
auto main(const int argc, char *argv[]) -> int {
auto app = MaintenanceApplication{argc, argv};
return app.run();
}
$ core/core_event_application
maintenance job started
maintenance job completed
$ core/core_event_application --help
Usage:
core_event_application [options]
Options:
-h, --help[=<keyword>] Display this help.
--version Display version information.
Starting Asynchronous Work after Option Parsing
initialize() is useful for metadata, terminal setup, shared services, and work that must exist before parsing.
registerCommandLineOptions() describes accepted arguments.
When asynchronous work depends on parsed values, override handleCommandLineOptions(const OptionValuesPtr &values).
The application calls this hook after successful parsing, before forwarding options to application parts and entering
main().
The passed values are non-null; help, version output, module overview, and parsing errors skip this hook.
Validate inputs and schedule the first event there so the base main() still supplies event dispatch.
For example, a directory packer can resolve its source and queue a callback that starts scanning:
void handleCommandLineOptions(const el::OptionValuesPtr &values) override {
const auto source = el::Path::fromNativeOrThrow(values->getText("source"_el))
.resolveOrThrow(el::PathResolveMode::Physical);
events()->invoke([this, source]() -> void { startScanning(source); });
}
Here source is a required text option registered by the application, and startScanning() begins its workflow on
the main loop.
A workflow using raw worker threads must retain their owners until those threads have been cancelled and joined.
Only override parseCommandLine() when the parsing behavior itself needs to change.
Queued initialization events are harmless when the user requests help or version output because the application never enters the event loop in that case. Avoid starting unmanaged background work during initialization, because it would need separate cancellation.
Finishing and Reporting Failures
The first call to quit() chooses the process exit code.
It also asks registered application parts and managed event threads to stop before the main event system exits.
An Erbsland Core exception thrown by a callback on the automatically managed main event loop crosses the same reporting
boundary as an exception from initialize() or main().
Use explicit result handling when a failure is expected and recoverable; throw ApplicationError when a callback must
terminate the whole process with a diagnostic and exit code.
Overriding main() disables the automatic event-loop entry.
Preparation that depends on options belongs in handleCommandLineOptions() and keeps that entry intact.
A custom main that also needs work after dispatch can call runEventLoop() explicitly.
Complete demo sources
core/EventApplication/main.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include <erbsland/Application.hpp>
#include <erbsland/Events.hpp>
#include <erbsland/Literals.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/Version.hpp>
namespace demo {
using namespace el::text::literals;
/// Derive from `Application` when the program is driven by asynchronous work on the main event loop.
///
/// The default `Application::main()` enters the event loop.
/// Calling `quit()` from an event callback finishes the loop and returns the requested exit code.
class MaintenanceApplication final : public el::Application {
public:
using Application::Application;
protected: // implement Application
void initialize() override {
info().setApplicationName("Maintenance Queue"_el);
info().setApplicationVersion(el::Version{1, 0, 0});
events()->invoke([this]() -> void { runMaintenance(); });
}
private:
void runMaintenance() {
el::io::printLine("maintenance job started"_el);
events()->invoke([this]() -> void {
el::io::printLine("maintenance job completed"_el);
quit();
});
}
};
/// Create the application and leave event dispatch to its default main implementation.
auto main(const int argc, char *argv[]) -> int {
auto app = MaintenanceApplication{argc, argv};
return app.run();
}
}
auto main(const int argc, char *argv[]) -> int {
return demo::main(argc, argv);
}