Errors
The ThreadPool module uses several complementary mechanisms to report failure:
ThreadPoolErrc
std::error_code
std::system_error
user exceptions
TaskStatus
TaskResult
bool submission resultsThe mechanism depends on the API being used.
For result-producing work:
auto future = pool.submit([](){
return 42;
});the Future exposes:
future.status();
future.result();
future.error();and:
future.get();either returns the value or throws the stored failure.
Error model
The high-level model is:
task succeeds
↓
Future::get()
↓
return value
user callable throws
↓
Future stores exception
↓
Future::get()
↓
rethrow original exception
ThreadPool operation error
↓
Future stores ThreadPoolErrc
↓
Future::get()
↓
throw std::system_errorThese cases should be handled differently when the application needs to distinguish them.
ThreadPoolErrc
ThreadPool-specific error codes are represented by:
vix::threadpool::ThreadPoolErrcThe available values are:
enum class ThreadPoolErrc : std::uint8_t
{
ok = 0,
invalid_argument = 1,
stopped = 2,
rejected = 3,
queue_full = 4,
timeout = 5,
cancelled = 6,
not_ready = 7,
not_supported = 8,
internal_error = 9
};The numeric values are part of the current public error-code definition.
Error codes
| Error | Value | Message |
|---|---|---|
ok | 0 | ok |
invalid_argument | 1 | invalid argument |
stopped | 2 | thread pool stopped |
rejected | 3 | task rejected |
queue_full | 4 | task queue full |
timeout | 5 | operation timed out |
cancelled | 6 | operation cancelled |
not_ready | 7 | operation not ready |
not_supported | 8 | operation not supported |
internal_error | 9 | internal thread pool error |
ok
vix::threadpool::ThreadPoolErrc::okmeans no ThreadPool error is stored.
A successfully completed Future normally reports:
status = completed
result = success
error = okFor example:
auto future = pool.submit([](){
return 42;
});
const int value = future.get();after successful completion:
future.error() == vix::threadpool::ThreadPoolErrc::ok;invalid_argument
vix::threadpool::ThreadPoolErrc::invalid_argumentrepresents an invalid API argument.
When stored in a Future shared state, it maps to:
TaskStatus::failed
TaskResult::failureThe current high-level ThreadPool::submit() and handle() paths do not currently publish this code themselves.
It remains part of the public error vocabulary and can also be stored manually through Promise::set_error().
stopped
vix::threadpool::ThreadPoolErrc::stoppedrepresents an operation associated with a stopped pool or executor.
When stored in a Future, it maps to:
TaskStatus::rejected
TaskResult::rejectedThe current high-level ThreadPool submission API does not preserve this distinction when submission fails because the pool is stopped.
Instead, submit() and handle() currently store:
vix::threadpool::ThreadPoolErrc::rejectedfor that path.
rejected
vix::threadpool::ThreadPoolErrc::rejectedis the main high-level error produced when submit() or handle() cannot submit work.
For example:
vix::threadpool::ThreadPool pool(1);
pool.shutdown();
auto future = pool.submit([](){
return 42;
});The Future is immediately ready with:
status = rejected
result = rejected
error = rejectedCalling:
future.get();throws:
std::system_errorwhose error code is ThreadPoolErrc::rejected.
queue_full
vix::threadpool::ThreadPoolErrc::queue_fullrepresents queue-capacity rejection.
When stored in a Future, it maps to:
TaskStatus::rejected
TaskResult::rejectedThe public error code exists, but the current high-level ThreadPool submission path does not preserve queue-full as a distinct Future error.
The current flow is:
worker queue rejects task
↓
Scheduler::submit() returns false
↓
ThreadPool::submit()
↓
Future error = rejectedTherefore a Future currently reports:
ThreadPoolErrc::rejectedrather than:
ThreadPoolErrc::queue_fullfor this path.
See Queue and Rejection Policies.
timeout
vix::threadpool::ThreadPoolErrc::timeoutmaps to:
TaskStatus::timed_out
TaskResult::timeoutA common high-level path is an expired deadline before the submitted callable begins:
vix::threadpool::TaskOptions options;
options.set_deadline(
vix::threadpool::Deadline::after(
std::chrono::milliseconds{0}
)
);
auto future = pool.submit([](){
return 42;
}, options);When the deadline is observed as expired before callable execution, the Future receives:
error = timeout
status = timed_out
result = timeoutand get() throws std::system_error.
Execution timeout has a current distinction
An execution timeout configured through:
options.set_timeout(
vix::threadpool::Timeout::milliseconds(1)
);does not forcibly interrupt the callable.
There is also an important current implementation distinction between the Future layer and low-level task classification.
For example:
auto future = pool.submit([](){
std::this_thread::sleep_for(
std::chrono::milliseconds{10}
);
return 42;
}, options);can currently produce:
Future:
status = completed
result = success
error = ok
value = 42
low-level worker task:
result = timeoutTherefore runtime timeout metrics can increase while the corresponding Future still reports success.
See Timeouts.
cancelled
vix::threadpool::ThreadPoolErrc::cancelledmaps to:
TaskStatus::cancelled
TaskResult::cancelledFor example, a handle cancelled before its callable begins can produce:
auto handle = pool.handle([](){
return 42;
});
handle.cancel();If cancellation is observed before the user callable starts, the Future state becomes cancelled.
Calling:
handle.get();then throws a std::system_error whose error code represents ThreadPoolErrc::cancelled.
Cancellation remains cooperative.
A cancellation request made after the user callable has already started does not guarantee that the Future will become cancelled.
See Cancellation.
not_ready
vix::threadpool::ThreadPoolErrc::not_readyrepresents an operation that cannot complete yet.
It is also the value returned by:
future.error();for an invalid Future with no shared state.
For example:
vix::threadpool::Future<int> future;
const auto error = future.error();gives:
ThreadPoolErrc::not_readyHowever, this does not mean the invalid Future contains a stored not_ready error.
It has no shared state at all.
Invalid Future state
A default-constructed Future:
vix::threadpool::Future<int> future;reports:
valid() false
ready() false
status() created
result() none
error() not_readyCalling:
future.get();does not throw a ThreadPool std::system_error.
It throws:
std::future_errorwith:
std::future_errc::no_stateThe same applies to:
future.wait();
future.wait_for(...);
future.wait_until(...);because those operations require a valid shared state.
not_supported
vix::threadpool::ThreadPoolErrc::not_supportedrepresents an unsupported operation.
When stored in a Future, it maps to:
TaskStatus::failed
TaskResult::failureThe current high-level ThreadPool task-submission path does not publish this error itself.
It remains part of the public error-code API.
internal_error
vix::threadpool::ThreadPoolErrc::internal_errormaps to:
TaskStatus::failed
TaskResult::failureIt is also the error value stored in a shared state when a user exception is captured.
This does not mean Future::get() converts a user exception into std::system_error.
The original exception is stored separately and has precedence during get().
ThreadPool error category
ThreadPool errors use a custom:
std::error_categoryavailable through:
vix::threadpool::threadpool_category();Its name is:
vix.threadpoolFor example:
const auto& category =
vix::threadpool::threadpool_category();
const char* name = category.name();name points to:
vix.threadpoolConvert to std::error_code
Use:
std::error_code error =
vix::threadpool::make_error_code(
vix::threadpool::ThreadPoolErrc::timeout
);The result contains:
category = vix.threadpool
value = 5
message = operation timed outImplicit std::error_code conversion
ThreadPoolErrc is registered as:
std::is_error_code_enum<
vix::threadpool::ThreadPoolErrc
>so this is also valid:
std::error_code error =
vix::threadpool::ThreadPoolErrc::cancelled;The resulting error code uses the ThreadPool category automatically.
Inspect an error code
For example:
std::error_code error =
vix::threadpool::ThreadPoolErrc::queue_full;
vix::print("category:", error.category().name());
vix::print("value:", error.value());
vix::print("message:", error.message());The values are:
category: vix.threadpool
value: 4
message: task queue fullHelper functions
Use:
vix::threadpool::is_ok(error);to check:
error == ThreadPoolErrc::okFor example:
if (vix::threadpool::is_ok(future.error()))
{
// No ThreadPool error is stored.
}Use:
vix::threadpool::is_error(error);for the opposite check:
error != ThreadPoolErrc::okThese helpers operate on ThreadPoolErrc, not directly on std::error_code.
Future state model
A Future<T> shares a state that can contain one of three completion forms:
value
exception
ThreadPoolErrcConceptually:
SharedState<T>
├── optional value
├── exception_ptr
├── ThreadPoolErrc
├── TaskStatus
├── TaskResult
└── ready flagThe first successful attempt to make the state ready wins.
Later completion attempts are ignored.
Successful value
When a Promise stores:
promise.set_value(42);the shared state becomes:
ready = true
error = ok
status = completed
result = success
value = 42future.get() returns the stored value.
Successful void completion
For:
vix::threadpool::Promise<void> promise;
auto future = promise.get_future();
promise.set_value();the Future becomes:
ready = true
error = ok
status = completed
result = successand:
future.get();returns normally.
Stored ThreadPool error
When:
promise.set_error(
vix::threadpool::ThreadPoolErrc::cancelled
);the shared state becomes ready with a mapped status and result.
Then:
future.get();throws:
std::system_errorusing:
make_error_code(ThreadPoolErrc::cancelled)Error-to-status mapping
When SharedState::set_error() is used, the mappings are:
ThreadPoolErrc | TaskStatus | TaskResult |
|---|---|---|
ok | completed | success |
cancelled | cancelled | cancelled |
timeout | timed_out | timeout |
rejected | rejected | rejected |
queue_full | rejected | rejected |
stopped | rejected | rejected |
invalid_argument | failed | failure |
not_ready | failed | failure |
not_supported | failed | failure |
internal_error | failed | failure |
This mapping is shared by value-producing and void Futures.
ThreadPoolErrc and TaskResult are different types
These types answer different questions.
ThreadPoolErrc describes the specific error:
Why did the asynchronous operation fail?TaskResult describes the broader outcome:
How did the task finish?For example:
ThreadPoolErrc::queue_full
ThreadPoolErrc::stopped
ThreadPoolErrc::rejectedall map to:
TaskResult::rejectedThe result groups several specific error reasons into one execution outcome.
ThreadPoolErrc and TaskStatus are also different
TaskStatus describes lifecycle state:
created
queued
running
completed
failed
cancelled
timed_out
rejectedThreadPoolErrc provides an error reason.
For example:
error = timeout
↓
status = timed_outwhile:
error = queue_full
↓
status = rejectedUser exceptions
If a submitted callable throws:
auto future = pool.submit([]() -> int {
throw std::runtime_error{"task failed"};
});the wrapper catches the exception and stores its:
std::exception_ptrin the shared state.
The state becomes:
status = failed
result = failure
error = internal_errorbut the original exception is retained separately.
Original user exception is rethrown
Calling:
future.get();on that Future rethrows:
std::runtime_error{"task failed"}It does not throw:
std::system_error{
make_error_code(ThreadPoolErrc::internal_error)
}because SharedState::get() checks the stored exception before checking the error code.
The retrieval order is:
ready
↓
already retrieved?
↓
stored exception?
├── yes → rethrow original exception
│
└── no
↓
ThreadPool error?
├── yes → throw std::system_error
│
└── no
↓
return valueerror() after a user exception
Even though get() rethrows the original user exception:
future.error();reports:
vix::threadpool::ThreadPoolErrc::internal_errorbecause set_exception() sets the error field to internal_error.
Therefore application code can observe:
status = failed
result = failure
error = internal_errorwhile get() still preserves the original exception type.
Catch user exceptions separately
For example:
try
{
const int result = future.get();
use(result);
}
catch (const std::runtime_error& error)
{
handle_task_failure(error);
}
catch (const std::system_error& error)
{
handle_threadpool_failure(error);
}This distinguishes:
exception thrown by user callablefrom:
ThreadPoolErrc stored by asynchronous infrastructurewhen those categories matter.
Inspect std::system_error
A ThreadPool infrastructure error can be handled as:
try
{
const int value = future.get();
use(value);
}
catch (const std::system_error& error)
{
if (error.code() ==
vix::threadpool::ThreadPoolErrc::cancelled)
{
handle_cancelled();
}
}Because ThreadPoolErrc converts to std::error_code, direct comparison is available through the standard error-code machinery.
Inspect category
When distinguishing ThreadPool errors from another std::system_error source:
catch (const std::system_error& error)
{
if (error.code().category() ==
vix::threadpool::threadpool_category())
{
handle_threadpool_error(error.code());
}
}The category identity is stable within the process through the singleton returned by:
threadpool_category();Submission errors from post()
ThreadPool::post() does not return a Future or error code.
Its error channel is:
boolFor example:
const bool accepted = pool.post([](){
perform_work();
});
if (!accepted)
{
handle_submission_failure();
}A false return means the work was not accepted or handled successfully.
post() does not expose the specific rejection reason
The current post() API does not distinguish through its return value between conditions such as:
pool stopped
queue rejected task
invalid empty callable
other scheduler rejectionThey all appear as:
falsewhen the high-level post operation fails.
Use runtime metrics when aggregate rejection counts are useful.
Use submit() when a per-operation asynchronous result is required.
Empty post() callable
This:
vix::threadpool::Executor::Task task;
const bool accepted = pool.post(task);returns:
falseNo ThreadPoolErrc::invalid_argument object is returned to the caller.
The current high-level post() contract is only boolean.
Submission errors from submit()
submit() always returns a valid Future after constructing its Promise.
When the ThreadPool rejects submission:
submit()
↓
cannot accept
↓
Promise::set_error(rejected)
↓
Future immediately readyThe caller can inspect:
if (future.error() ==
vix::threadpool::ThreadPoolErrc::rejected)
{
// Submission was rejected.
}or consume it with get().
Submission errors from handle()
handle() follows the same Future error model:
auto handle = pool.handle([](){
return 42;
});Inspect:
handle.status();
handle.result();
handle.error();These forward to the underlying Future.
A handle can remain structurally valid even when submission was rejected because it still contains:
valid task ID
valid Future
CancellationSourceTherefore:
handle.valid()does not mean:
task was accepted by the ThreadPoolInspect its asynchronous result.
Cancellation before submission
For submit(), a task option can already contain a cancelled token:
vix::threadpool::CancellationSource source;
source.request_cancel();
vix::threadpool::TaskOptions options;
options.set_cancellation(source.token());
auto future = pool.submit([](){
return 42;
}, options);If the pool itself accepts submissions, the pre-run option check produces:
error = cancelled
status = cancelled
result = cancelledwithout constructing a worker task for execution.
Cancellation precedence over deadline
The current pre-run mapping is:
mergedOptions.cancellation.cancelled()
? ThreadPoolErrc::cancelled
: ThreadPoolErrc::timeoutTherefore when:
cancellation already requested
and
deadline already expiredthe Future receives:
ThreadPoolErrc::cancelledat that high-level pre-run check.
Deadline before callable execution
Even after task submission, the submit() wrapper checks the observed absolute deadline before invoking the callable.
If expired:
Future error = timeout
Future status = timed_out
Future result = timeoutThe callable is not invoked.
Running cancellation can still produce success
The high-level wrapper checks cancellation before calling the user function.
It does not check that cancellation token again after the function returns.
Therefore:
callable begins
↓
cancellation requested
↓
callable ignores cancellation
↓
returns 42
↓
Future stores value 42can result in:
status = completed
result = success
error = okeven though:
handle.cancelled();reports that cancellation was requested.
Cancellation request state and Future result state are different concepts.
Future::wait_for() timeout is not a task timeout
This is an important distinction:
const auto status = future.wait_for(
std::chrono::milliseconds{10}
);can return:
std::future_status::timeoutThis means only:
the caller waited 10 ms
and
the Future was not ready yetIt does not:
set Future error to ThreadPoolErrc::timeout
cancel the task
change TaskStatus to timed_out
change TaskResult to timeoutThe Future continues running normally.
wait_until() has the same distinction
Similarly:
const auto status = future.wait_until(deadline);returning:
std::future_status::timeoutonly describes the caller-side wait operation.
It is independent from:
TaskOptions timeout
TaskOptions deadline
ThreadPoolErrc::timeout
TaskStatus::timed_out
TaskResult::timeoutFuture retrieval errors
Future::get() can also throw standard Future errors unrelated to ThreadPoolErrc.
The main cases are:
invalid Future
↓
std::future_error(no_state)
get() already called once
↓
std::future_error(future_already_retrieved)These are standard Future object-state errors.
They are not ThreadPool task execution errors.
get() consumes the result
For example:
auto future = pool.submit([](){
return 42;
});
const int first = future.get();A second:
future.get();throws:
std::future_errorwith:
std::future_errc::future_already_retrievedThe Future object can still report its stored:
status
result
errorafter retrieval, but the value cannot be retrieved a second time.
Promise retrieval errors
A Promise can produce its Future only once:
vix::threadpool::Promise<int> promise;
auto future = promise.get_future();Calling:
promise.get_future();again throws:
std::future_error{
std::future_errc::future_already_retrieved
}A moved-from Promise with no state also throws:
std::future_error{
std::future_errc::no_state
}when an operation requiring state is used.
Promise error completion
A Promise can explicitly complete a Future with a ThreadPool error:
vix::threadpool::Promise<int> promise;
auto future = promise.get_future();
promise.set_error(
vix::threadpool::ThreadPoolErrc::not_supported
);The Future becomes:
status = failed
result = failure
error = not_supportedand:
future.get();throws std::system_error.
Promise exception completion
Use:
promise.set_exception(
std::make_exception_ptr(
std::runtime_error{"failure"}
)
);or inside a catch block:
promise.set_current_exception();The Future becomes:
status = failed
result = failure
error = internal_errorwhile get() rethrows the original captured exception.
First completion wins
SharedState completion methods ignore calls made after the state is already ready.
For example:
promise.set_value(42);
promise.set_error(
vix::threadpool::ThreadPoolErrc::cancelled
);the second operation does not replace the successful value.
The result remains:
value = 42
status = completed
result = success
error = okThis behavior prevents competing completion paths from overwriting an already published asynchronous result.
Do not use set_error(ok) to provide a value
For Promise<T>, successful completion should use:
promise.set_value(value);or:
promise.emplace_value(...);Do not use:
promise.set_error(
vix::threadpool::ThreadPoolErrc::ok
);as a replacement for set_value().
set_error(ok) marks the state ready and maps it to successful status/result, but it does not store a T value.
A value-producing Future requires an actual value before successful retrieval.
Treat ThreadPoolErrc::ok as the absence of an error, not as the value-completion operation for Promise<T>.
No automatic broken-promise completion
The custom Vix Promise destructor does not publish:
std::future_errc::broken_promisewhen an unresolved producer disappears.
This differs from a behavior developers may expect from std::promise.
For example, a queued wrapper removed by:
pool.clear();can destroy the Promise responsible for a Future without making that Future ready.
The Future can remain unresolved.
Clear and unresolved Futures
Conceptually:
submit()
↓
Future returned
↓
task waiting in queue
↓
pool.clear()
↓
task wrapper destroyed
↓
producer disappears
↓
Future still non-readyCalling:
future.get();can then block indefinitely.
Non-draining shutdown has the same risk
With:
config.drain_on_shutdown = false;a queued result-producing task may never execute before workers stop.
Its Future is not automatically converted into:
rejected
cancelled
broken_promiseand can remain non-ready.
When Future completion is required, coordinate work before non-draining shutdown or use the default draining lifecycle.
High-level rejection reason is currently coarse
The public error vocabulary contains:
stopped
rejected
queue_fullbut high-level ThreadPool::submit() currently reduces scheduler submission failure to:
ThreadPoolErrc::rejectedThe same is true for handle().
Therefore application code currently cannot use a returned Future to distinguish:
queue full
from
scheduler rejection
from
completed pool shutdownthrough separate ThreadPoolErrc values.
This distinction may exist in lower-level runtime state or context, but it is not preserved by the high-level Future submission API.
Error inspection before get()
Because status, result, and error are exposed separately, callers can inspect a ready Future before consuming it:
future.wait();
if (future.error() ==
vix::threadpool::ThreadPoolErrc::cancelled)
{
handle_cancelled();
}
else
{
const auto value = future.get();
use(value);
}Remember that a user exception reports:
ThreadPoolErrc::internal_errorthrough error(), while the exact exception type is available only by calling get() and catching it.
Error handling example
#include <iostream>
#include <system_error>
#include <vix/threadpool/all.hpp>
int main()
{
vix::threadpool::ThreadPool pool(2);
auto future = pool.submit([]() -> int {
throw std::runtime_error{"task failed"};
});
try
{
const int value = future.get();
std::cout << value << '\n';
}
catch (const std::runtime_error& error)
{
std::cout << "task: " << error.what() << '\n';
}
catch (const std::system_error& error)
{
std::cout << "threadpool: "
<< error.code().message()
<< '\n';
}
return 0;
}The std::runtime_error thrown by the callable is preserved rather than replaced with a generic ThreadPool exception.
Rejection example
#include <iostream>
#include <system_error>
#include <vix/threadpool/all.hpp>
int main()
{
vix::threadpool::ThreadPool pool(1);
pool.shutdown();
auto future = pool.submit([](){
return 42;
});
if (future.error() !=
vix::threadpool::ThreadPoolErrc::rejected)
{
return 1;
}
try
{
(void)future.get();
}
catch (const std::system_error& error)
{
std::cout << error.code().category().name()
<< ": "
<< error.code().message()
<< '\n';
return 0;
}
return 1;
}The error code contains:
category = vix.threadpool
value = 3
message = task rejectedError paths by API
| API | Failure reporting |
|---|---|
ThreadPool::post() | false |
ThreadPool::submit() | Future |
ThreadPool::handle() | TaskHandle containing Future |
Future::get() user exception | Rethrows original exception |
Future::get() ThreadPool error | Throws std::system_error |
| Invalid Future operation | Throws std::future_error |
Future::wait_for() caller timeout | Returns std::future_status::timeout |
Promise::set_error() | Stores ThreadPoolErrc |
Promise::set_exception() | Stores std::exception_ptr |
Low-level Task | TaskStatus and TaskResult |
| Runtime observation | Metrics and statistics |
Handling errors by intent
When the operation is fire-and-forget:
if (!pool.post(task))
{
handle_submission_failure();
}When a value is required:
try
{
auto value = future.get();
}
catch (const std::system_error& error)
{
// ThreadPool error.
}
catch (...)
{
// User callable exception.
}When cancellation matters:
if (handle.error() ==
vix::threadpool::ThreadPoolErrc::cancelled)
{
handle_cancelled();
}When only readiness should be bounded:
if (future.wait_for(
std::chrono::milliseconds{100}
) == std::future_status::timeout)
{
// Caller stopped waiting after 100 ms.
// The task itself was not timed out by this operation.
}Choose the error mechanism according to the layer being observed.
Error model summary
The Future path is:
ThreadPool::submit()
↓
Promise + Future
↓
submission accepted?
┌────┴────┐
no yes
│ │
rejected ▼
error pre-run checks
↓
cancelled/deadline?
┌───┴───┐
yes no
│ │
error ▼
user callable
┌──┴──┐
throws returns
│ │
exception value
└──┬────┘
↓
Future ready
↓
get()
┌───────┼────────┐
▼ ▼ ▼
exception error value
│ │ │
rethrow system return
errorThe important properties are:
ThreadPoolErrcis the module's public error-code enum.- The current values range from
ok = 0throughinternal_error = 9. - ThreadPool error codes use the
vix.threadpoolerror category. ThreadPoolErrcconverts tostd::error_code.make_error_code()can be used explicitly.is_ok()andis_error()provide simple enum checks.SharedStatemaps ThreadPool errors toTaskStatusandTaskResult.cancelledmaps to cancelled status/result.timeoutmaps to timed-out status/result.rejected,queue_full, andstoppedall map to rejected status/result.invalid_argument,not_ready,not_supported, andinternal_errormap to failed/failure.Future::get()throwsstd::system_errorfor stored ThreadPool errors.- User callable exceptions are stored separately and rethrown with their original type.
- A user exception also sets the Future's ThreadPool error field to
internal_error. - Exception rethrow has precedence over conversion of
internal_errorintostd::system_error. post()exposes only a boolean submission result.- The high-level
submit()andhandle()APIs currently report scheduler submission failures asThreadPoolErrc::rejected. - They do not currently preserve
queue_fullorstoppedas separate Future errors. - An invalid Future reports
not_readythrougherror(), but operations such asget()throwstd::future_error(no_state). get()can be called only once and later retrieval throwsstd::future_error(future_already_retrieved).wait_for()andwait_until()caller timeouts do not modify the task or Future result.- An expired task deadline can produce
ThreadPoolErrc::timeout. - Execution timeout currently has a known Future versus low-level task-classification distinction.
- Cancellation is cooperative and a cancellation request does not guarantee a cancelled Future after execution has already started.
- Promise completion is first-writer-wins once the shared state becomes ready.
- Use
set_value()for successfulPromise<T>completion, notset_error(ok). - The custom Promise does not currently produce automatic
broken_promisecompletion when an unresolved producer disappears. clear()and non-draining shutdown can therefore leave result-producing Futures non-ready.- Error handling should distinguish infrastructure errors, user exceptions, object-state errors, and caller-side wait timeouts.
Continue with CMake for linking the ThreadPool module from CMake projects.