Elegant Promise/Future pattern bringing modern async programming to C++17 with minimal overhead
AsyncOp is a lightweight C++ library that provides Promise/Future semantics for asynchronous programming. It eliminates callback hell through chainable operations while maintaining minimal memory footprint and CPU overhead.
Perfect for:
- Event-driven applications (GLib/Qt)
- Resource-constrained environments
- Network and I/O operations
- Embedded Linux systems
- Any C++17 codebase needing clean async patterns
✨ Modern API - Clean, chainable syntax inspired by JavaScript Promises
🔗 Type-Safe Chaining - Transform AsyncOp<T> → AsyncOp<U> seamlessly
🛡️ Robust Error Handling - Multiple recovery strategies with automatic propagation
🔄 Collection Operations - Process multiple async operations (sequential or parallel)
🧵 Thread-Safe - Safe worker thread integration with main event loop marshaling
⚡ Zero Dependencies - Only requires C++17, spdlog, and GLib/Qt (for event loop)
📦 Embedded-Friendly - Optimized memory layout, minimal allocations
🎯 Single Responsibility - One callback per operation (prevents handler explosion)
#include "async_op.hpp"
// Fetch user, then posts, with fallback
fetchUserAsync(userId)
.then([](User user) {
return fetchPostsAsync(user.id); // Chain async operations
})
.recover([](ao::ErrorCode err) {
return getCachedPosts(); // Fallback on error
})
.then([](std::vector<Post> posts) {
displayPosts(posts);
})
.onError([](ao::ErrorCode err) {
spdlog::error("Failed: {}", err);
});That's it! No nested callbacks, clean error handling, easy to read and maintain.
- C++17 or later
- Event Loop: GLib 2.0+ (default) or Qt 5.12+ (define
ASYNC_USE_QT) - spdlog (includes fmt for formatting)
# Clone repository
git clone https://github.com/pansz/asyncop.git
cd asyncop
# Build
mkdir build && cd build
cmake ..
make
# Run examples
./build/examples/example_basic_glibCMakeLists.txt Integration:
# Add AsyncOp headers
include_directories(${CMAKE_SOURCE_DIR}/include)
# Link dependencies (GLib example)
find_package(PkgConfig REQUIRED)
pkg_check_modules(GLIB REQUIRED glib-2.0)
find_package(spdlog REQUIRED)
target_link_libraries(your_app ${GLIB_LIBRARIES} spdlog::spdlog)ao::AsyncOp<Data> fetchDataAsync() {
auto promise = ao::makePromise<Data>();
// Schedule async work
ao::add_timeout(100ms, [promise]() {
promise->resolveWith(Data{42});
return false;
});
return ao::AsyncOp<Data>(promise);
}fetchValue()
.then([](int x) { return x * 2; }) // Transform
.then([](int x) { return std::to_string(x); }) // Change type
.then([](std::string s) {
spdlog::info("Result: {}", s);
});fetchFromServer()
.recover([](ao::ErrorCode err) {
return getCachedData(); // Error → Success
})
.then([](Data d) {
processData(d); // Handles both server and cache results
});std::vector<int> ids = {1, 2, 3, 4, 5};
ao::mapParallel(ids, [](int id) {
return fetchUserAsync(id);
})
.then([](std::vector<User> users) {
displayUsers(users);
});operation()
.tap([](Data d) {
spdlog::debug("Got: {} bytes", d.size());
})
.tapError([](ao::ErrorCode e) {
metrics.increment("errors");
})
.then([](Data d) { return process(d); });| Method | Purpose | Example |
|---|---|---|
.then(f) |
Transform success value | .then([](int x) { return x * 2; }) |
.recover(f) |
Convert error to success | .recover([](ErrorCode e) { return fallback(); }) |
.next(s, e) |
Handle both paths | .next(onSuccess, onError) |
.onError(f) |
Terminal error handler | .onError([](ErrorCode e) { log(e); }) |
.tap(f) |
Side effect (success) | .tap([](T v) { log(v); }) |
.tapError(f) |
Side effect (error) | .tapError([](ErrorCode e) { metrics++; }) |
.timeout(d) |
Add timeout | .timeout(5000ms) |
.filter(s, e) |
Validate/filter paths | .filter(validate, recover) |
.finally(f) |
Cleanup handler | .finally([]() { cleanup(); }) |
| Function | Behavior | Use Case |
|---|---|---|
all(ops) |
Wait for all success | Batch operations |
any(ops) |
First success wins | Redundant servers |
race(ops) |
First to settle wins | Timeout pattern |
allSettled(ops) |
Wait for all (success + error) | Best-effort batch |
map(items, f) |
Sequential transform | Rate-limited processing |
mapParallel(items, f) |
Parallel transform | Fast batch loading |
// ❌ Traditional callback style
fetchUser(userId, [](User user) {
fetchPosts(user.id, [](Posts posts) {
fetchComments(posts[0].id, [](Comments comments) {
display(comments);
}, [](Error e) { handleError(e); });
}, [](Error e) { handleError(e); });
}, [](Error e) { handleError(e); });// ✅ Clean, readable, maintainable
fetchUser(userId)
.then([](User u) { return fetchPosts(u.id); })
.then([](Posts p) { return fetchComments(p[0].id); })
.then([](Comments c) { display(c); })
.onError([](ErrorCode e) { handleError(e); });AsyncOp is built with these principles:
- Minimal Overhead - Optimized memory layout, efficient allocations
- Resource Awareness - Provides sequential operations for constrained environments
- Type Safety - Compile-time type checking, no runtime type erasure surprises
- Single Responsibility - One callback per operation prevents memory explosion
- Error Transparency - Errors visible and handleable at every stage
- Event Loop Agnostic - Works with GLib, Qt, or custom event loops
Memory Optimization Example:
// ✅ Efficient - uses onSuccess() (no unused AsyncOp allocation)
op.then(process).onSuccess([](Result r) { display(r); });
// ❌ Less efficient - creates unused AsyncOp
op.then(process).then([](Result r) { display(r); });📖 Complete Documentation - Comprehensive guide with:
- Detailed API reference for all methods
- Advanced patterns (retry, branching, conditional execution)
- Collection operations guide
- Thread safety & integration
- Performance considerations
- Troubleshooting guide
🚀 Examples - Working code samples:
- Basic chaining and error handling
- Network request patterns
- Message registry (request-response correlation)
- Worker thread integration
- Collection operations
AsyncOp is designed for single-threaded event loops but provides safe cross-thread integration:
ao::AsyncOp<Result> computeInBackground() {
auto promise = ao::makePromise<Result>();
std::thread([promise]() {
Result r = heavyComputation();
// Marshal back to main thread
ao::invoke_main([promise, r]() {
promise->resolveWith(r);
});
}).detach();
return ao::AsyncOp<Result>(promise);
}Thread-safe operations:
- Creating AsyncOp/Promise ✅
- Querying state (
isPending(), etc.) ✅ add_timeout(),add_idle(),invoke_main()✅
Main thread required:
- Settling promises (
resolveWith(),rejectWith())⚠️ - Executing callbacks
⚠️
#include "async_op.hpp"
int main() {
GMainLoop* loop = g_main_loop_new(NULL, FALSE);
// Your async operations
g_main_loop_run(loop);
}// Define ASYNC_USE_QT before including
#include <QCoreApplication>
#include "async_op.hpp"
int main(int argc, char** argv) {
QCoreApplication app(argc, argv);
// Your async operations
return app.exec();
}Extend ao_event_loop.hpp with your backend. See Integration Guide.
Memory per AsyncOp:
- State object: ~120-200 bytes (varies with
T) - Shared via
shared_ptr: Multiple AsyncOps can share state
Operation overhead:
- Timer creation: ~1-5 μs (GLib/Qt)
- Callback invocation: ~100 ns
Collection operations:
all(),mapParallel(): O(n) parallelmap(),forEach(): O(n) sequentialany(),race(): O(1) best case
Optimization tips:
- Use
onSuccess()/onError()for terminal handlers (saves allocation) - Use
std::move()for large objects - Prefer
mapParallel()for independent operations - Use
map()for rate-limited sequential processing
| Feature | AsyncOp | Folly Futures | Boost.Asio | JavaScript Promises |
|---|---|---|---|---|
| C++ Version | C++17 | C++14 | C++11 | N/A |
| Dependencies | Minimal | Heavy | Moderate | N/A |
| Memory Footprint | Small | Large | Medium | N/A |
| Event Loop | GLib/Qt | Custom | Built-in | Browser/Node |
| Learning Curve | Low | Medium | High | Low |
| Embedded-Friendly | ✅ Yes | ❌ No | N/A |
# GLib backend (default)
cmake ..
make
# Qt backend
cmake -DASYNC_USE_QT=ON ..
make
# With examples
cmake -DBUILD_EXAMPLES=ON ..
make
# With tests
cmake -DBUILD_TESTS=ON ..
make test# All tests
ctest
# Specific backend (after running `make`)
./build/tests/test_asyncop_glib
./build/tests/test_asyncop_qtfetchUser(id)
.then([](User u) { return fetchProfile(u.profileId); })
.then([](Profile p) { displayProfile(p); })
.onError([](ErrorCode e) { spdlog::error("Error: {}", e); });ao::retryWithBackoff<Data>(
[]() { return fetchFromAPI(); },
3, // max attempts
1000ms // initial delay (exponential backoff)
)
.then([](Data d) { processData(d); });fetchFromServer()
.timeout(5000ms)
.recover([](ErrorCode e) {
if (e == ErrorCode::Timeout) {
return getCachedData();
}
throw e;
});std::vector<URL> urls = getURLs();
ao::mapParallel(urls, [](URL url) {
return fetchURL(url);
})
.then([](std::vector<Data> results) {
spdlog::info("Fetched {} URLs", results.size());
});asyncop/
├── include/
│ ├── async_op.hpp # Main AsyncOp implementation
│ ├── async_op_void.hpp # Specialization for AsyncOp<void>
│ ├── ao_event_loop.hpp # Event loop abstraction (GLib/Qt)
│ └── msg_registry.hpp # Message-based async tracker (optional)
├── docs/
│ └── async_op_doc.md # Complete documentation
├── examples/
│ ├── CMakeLists.txt
│ ├── example_callback_conversion.cpp
│ ├── example_message_registry.cpp
│ └── example_qt_http.cpp
├── tests/
│ ├── CMakeLists.txt
│ ├── test_asyncop.cpp # Shared test implementation
│ ├── testmain_glib.cpp # GLib backend test entry point
│ └── testmain_qt.cpp # Qt backend test entry point
├── CMakeLists.txt # Root CMake configuration
├── README.md # Project overview
├── LICENSE # MIT license
├── CHANGELOG.md # Version history
└── CONTRIBUTING.md # Contribution guidelines
We welcome contributions! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Areas we'd love help with:
- Additional event loop backends (libuv, asio, etc.)
- Performance optimizations
- More examples and use cases
- Documentation improvements
MIT License - See LICENSE file for details.
AsyncOp was inspired by:
- JavaScript Promises - for elegant chaining API
- Folly Futures - for collection operations
- Boost.Asio - for async I/O patterns
- The C++ community - for ongoing feedback and ideas
Special thanks to all contributors and users who have helped shape this library.
Current Version: 2.4.1 Release Date: 2026-02-28 Author: pansz
Changelog:
- v2.4.1: Added
filterSuccess(),filterError(), fixednext()nullptr handling - v2.4.0: Added
filter(),cancel()methods; deprecatedorElse(),recoverFrom() - v2.3.2: Added debug assert for callback overwrite violations
- v2.3.1: Fixed race conditions in parallel batch operations
- v2.3.0: Added
onSuccess(), improved callback protection - v2.2.0: Initial release with
timeout(),tap(),finally(), collection ops
- 📖 Documentation: docs/async_op_doc.md
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
- ✉️ Email: pan.sz@outlook.com
⭐ Star this repo if AsyncOp helps your project! ⭐
Made with ❤️ for the C++ community