Task Results and Status
The ThreadPool module uses TaskStatus and TaskResult to describe two different aspects of task execution.
TaskStatus answers:
Where is the task in its lifecycle?TaskResult answers:
How did the execution finish?For result-producing work, ThreadPoolErrc can additionally describe the ThreadPool-specific error associated with the asynchronous result.
TaskStatus
↓
lifecycle state
TaskResult
↓
execution outcome
ThreadPoolErrc
↓
ThreadPool-specific errorThese types are related, but they are not interchangeable.
TaskStatus
TaskStatus describes the lifecycle state of a task.
The available values are:
enum class TaskStatus : std::uint8_t
{
created,
queued,
running,
completed,
failed,
cancelled,
timed_out,
rejected
};A low-level task normally progresses through:
created
↓
queued
↓
running
↓
┌───────────┬───────────┬───────────┬───────────┐
▼ ▼ ▼ ▼
completed failed cancelled timed_outA task can also reach:
rejectedwithout executing.
created
A task starts in the created state.
vix::threadpool::Task task(
vix::threadpool::TaskId{1},
vix::threadpool::TaskFunction([](){
// Work.
})
);
const auto status = task.status();At this point:
status = created
result = noneThe task exists but has not entered a worker queue.
A newly created asynchronous Future state also starts with:
status = created
result = nonequeued
A task becomes queued after it has been accepted for execution and inserted into a worker queue.
Conceptually:
Task
↓
Scheduler
↓
Worker
↓
TaskQueue
↓
queuedThe task has not started executing yet.
The low-level Task object records this transition through:
task.mark_queued();Ordinary application code does not need to call mark_queued() because the worker runtime manages this transition.
running
A task becomes running immediately before its callable is invoked.
queued
↓
worker removes task from queue
↓
running
↓
callable executesFor low-level Task, this transition happens inside:
task.run();A running task has started consuming worker execution time.
Terminal states
A status is terminal when the task will no longer move to another lifecycle state.
The terminal statuses are:
completed
failed
cancelled
timed_out
rejectedUse:
if (vix::threadpool::is_terminal(status))
{
// Final task state.
}is_terminal() returns false for:
created
queued
runningand true for every final state.
Active states
The helper:
vix::threadpool::is_active(status);returns true only for:
queued
runningFor example:
const auto status = task.status();
if (vix::threadpool::is_active(status))
{
// The task is queued or executing.
}created is not considered active because the task has not entered execution yet.
Terminal states are also not active.
completed
completed means that the task finished successfully.
For a low-level task:
status = completed
result = successFor a successful Future:
ready = true
status = completed
result = success
error = okFor example:
auto future = pool.submit([](){
return 42;
});
const int value = future.get();A successful result-producing submission completes its asynchronous state with TaskStatus::completed.
failed
failed means that task execution ended because of an execution failure.
For a low-level task whose callable throws:
status = failed
result = failureThe exception is captured by the task.
For result-producing work:
auto future = pool.submit([]() -> int {
throw std::runtime_error("failure");
});the Future state becomes:
status = failed
result = failure
error = internal_errorand get() rethrows the original C++ exception.
failed therefore describes the lifecycle outcome. It does not replace the original exception.
cancelled
cancelled means that execution ended through the cancellation path.
The corresponding result is:
status = cancelled
result = cancelledFor a Future completed with a cancellation error:
status = cancelled
result = cancelled
error = cancelledCancellation is cooperative and has its own timing rules.
See Cancellation.
timed_out
timed_out represents a task whose execution timing condition was exceeded.
The matching result is:
status = timed_out
result = timeoutFor an asynchronous state completed with ThreadPoolErrc::timeout:
status = timed_out
result = timeout
error = timeoutTimeout and deadline behavior are described separately because timing can be observed at different stages of execution.
rejected
rejected means that work did not enter normal execution.
For a low-level task:
status = rejected
result = rejectedFor a result-producing submission, several ThreadPool errors map to this state:
rejected
queue_full
stoppedFor example:
vix::threadpool::ThreadPool pool(1);
pool.shutdown();
auto future = pool.submit([](){
return 42;
});The asynchronous result is:
status = rejected
result = rejected
error = rejectedThe callable is not executed.
TaskResult
TaskResult describes how a task execution attempt ended.
The available values are:
enum class TaskResult : std::uint8_t
{
none,
success,
failure,
cancelled,
timeout,
rejected
};Unlike TaskStatus, TaskResult does not describe intermediate lifecycle stages such as queued or running.
It describes the outcome.
none
none means that no terminal execution result has been recorded.
A newly created task starts with:
status = created
result = noneA non-ready Future state also reports:
status = created
result = nonenone is not a successful or failed execution result.
success
success means that execution completed normally.
status = completed
result = successUse:
if (vix::threadpool::is_success(result))
{
// Successful task result.
}is_success() returns true only for:
successFailure results
The helper:
vix::threadpool::is_failure(result);returns true for:
failure
cancelled
timeout
rejectedand returns false for:
none
successThis is important because failure is only one member of the broader set of unsuccessful terminal outcomes.
Conceptually:
TaskResult
├── none
├── success
└── unsuccessful
├── failure
├── cancelled
├── timeout
└── rejectedTask::succeeded()
A low-level Task provides:
if (task.succeeded())
{
// result == TaskResult::success
}This is equivalent to checking:
task.result() == vix::threadpool::TaskResult::successTask::failed()
A low-level Task also provides:
if (task.failed())
{
// Unsuccessful terminal result.
}Despite its name, Task::failed() is broader than:
result == failureIt uses is_failure() and therefore returns true for:
failure
cancelled
timeout
rejectedIt returns false for:
none
successWhen code needs one specific outcome, compare TaskResult directly.
For example:
if (task.result() == vix::threadpool::TaskResult::cancelled)
{
// Specifically cancelled.
}Status and result mappings
The normal relationship between terminal status and result is:
TaskStatus | TaskResult |
|---|---|
created | none |
queued | none |
running | none |
completed | success |
failed | failure |
cancelled | cancelled |
timed_out | timeout |
rejected | rejected |
The distinction is useful because lifecycle and outcome answer different questions.
For example:
queuedis meaningful as a status but there is no equivalent queued TaskResult, because execution has not finished.
Inspect a low-level Task
A low-level Task exposes both views:
const auto status = task.status();
const auto result = task.result();For example:
vix::threadpool::Task task(
vix::threadpool::TaskId{1},
vix::threadpool::TaskFunction([](){
// Work.
})
);
const auto before_status = task.status();
const auto before_result = task.result();
const auto execution_result = task.run();
const auto after_status = task.status();
const auto after_result = task.result();For successful execution:
before:
status = created
result = none
after:
status = completed
result = successTask::run() itself returns the final TaskResult.
Convenience lifecycle checks
Task exposes several convenience operations:
task.done();
task.running();
task.queued();
task.succeeded();
task.failed();They correspond to:
done()
↓
status is terminal
running()
↓
status == running
queued()
↓
status == queued
succeeded()
↓
result == success
failed()
↓
result is failure, cancelled, timeout, or rejectedUse the direct status() or result() values when the exact state matters.
Readable names
Both enums provide to_string() helpers.
For status:
const char* name = vix::threadpool::to_string(
vix::threadpool::TaskStatus::running
);The result is:
runningAvailable status strings are:
created
queued
running
completed
failed
cancelled
timed_out
rejectedFor results:
const char* name = vix::threadpool::to_string(
vix::threadpool::TaskResult::success
);The result is:
successAvailable result strings are:
none
success
failure
cancelled
timeout
rejectedUnknown enum values return:
unknownFuture status and result
Future<T> also exposes:
const auto status = future.status();
const auto result = future.result();These values belong to the Future's shared asynchronous state.
For a normal successful submission:
auto future = pool.submit([](){
return 42;
});
future.wait();
const auto status = future.status();
const auto result = future.result();the values are:
status = completed
result = successFuture state is not a live Task state mirror
The low-level Task and the Future shared state are separate objects.
A low-level task can move through:
created
queued
running
completedwhile a Future shared state normally remains:
createduntil a terminal asynchronous result is published.
Conceptually:
low-level Task:
created → queued → running → completed
Future state:
created ───────────────────→ completedTherefore:
future.status();should not be used to determine whether its worker task is currently queued or running.
The current Future model primarily exposes the asynchronous result state.
Future before completion
Before a Future becomes ready:
auto future = pool.submit([](){
return 42;
});its shared state starts as:
ready = false
status = created
result = none
error = okThe result may become ready immediately on another worker, so these values should be treated as snapshots when the pool is running concurrently.
Use:
future.ready();to determine whether a terminal asynchronous result has been published.
Invalid Future
A default-constructed Future has no shared state:
vix::threadpool::Future<int> future;Its inspection methods report:
valid() false
ready() false
status() created
result() none
error() not_readyThe created status here is a fallback value. It does not mean that an actual task exists.
Check:
future.valid();when the distinction matters.
ThreadPoolErrc
ThreadPoolErrc describes errors specific to the module.
The values are:
ok
invalid_argument
stopped
rejected
queue_full
timeout
cancelled
not_ready
not_supported
internal_errorA Future exposes the stored value through:
const auto error = future.error();This provides more information than TaskResult when several errors map to the same execution outcome.
Error to status mapping
When a Promise completes a Future using:
promise.set_error(error);the error is converted into a TaskStatus.
The mapping is:
ThreadPoolErrc | TaskStatus |
|---|---|
ok | completed |
cancelled | cancelled |
timeout | timed_out |
rejected | rejected |
queue_full | rejected |
stopped | rejected |
invalid_argument | failed |
not_ready | failed |
not_supported | failed |
internal_error | failed |
This means several distinct errors can share one lifecycle status.
For example:
queue_full
↓
status = rejectedand:
stopped
↓
status = rejectedThe exact reason remains available through error().
Error to result mapping
The same error is also converted into TaskResult.
ThreadPoolErrc | TaskResult |
|---|---|
ok | success |
cancelled | cancelled |
timeout | timeout |
rejected | rejected |
queue_full | rejected |
stopped | rejected |
invalid_argument | failure |
not_ready | failure |
not_supported | failure |
internal_error | failure |
This gives three levels of detail:
ThreadPoolErrc::queue_full
↓
TaskStatus::rejected
↓
TaskResult::rejectedThe error explains why. The status explains the lifecycle outcome. The result categorizes the execution outcome.
Successful Future
A successful result-producing task normally reports:
ready() true
status() completed
result() success
error() okFor example:
auto future = pool.submit([](){
return 42;
});
future.wait();
if (
future.status() == vix::threadpool::TaskStatus::completed &&
future.result() == vix::threadpool::TaskResult::success &&
future.error() == vix::threadpool::ThreadPoolErrc::ok
)
{
// Successful asynchronous result.
}For ordinary application code, get() is usually simpler when the value itself is required.
Failed Future with an exception
When a submitted callable throws:
auto future = pool.submit([]() -> int {
throw std::runtime_error("failure");
});the Future state becomes:
ready() true
status() failed
result() failure
error() internal_errorCalling:
future.get();rethrows the original exception.
The original C++ exception therefore contains more specific failure information than ThreadPoolErrc::internal_error.
Cancelled Future
A Future completed through the cancellation error path reports:
ready() true
status() cancelled
result() cancelled
error() cancelledThe callable may not have executed, depending on when cancellation was observed.
See Cancellation.
Timed-out Future
A Future explicitly completed with:
vix::threadpool::ThreadPoolErrc::timeoutreports:
ready() true
status() timed_out
result() timeout
error() timeoutTiming behavior is handled at several execution layers, so use Timeouts and Deadlines for the complete contract.
Rejected Future
A result-producing submission rejected before normal execution reports:
ready() true
status() rejected
result() rejectedThe error describes the reason.
For example:
error = rejectedor, for a manually produced asynchronous state:
error = queue_full
error = stoppedAll of these map to the same status and result category.
Why keep all three?
Consider a queue-capacity failure.
Only looking at TaskResult gives:
rejectedLooking at TaskStatus also gives:
rejectedLooking at ThreadPoolErrc can identify:
queue_fullThe three levels serve different purposes:
TaskStatus
↓
What lifecycle state was reached?
TaskResult
↓
What broad outcome occurred?
ThreadPoolErrc
↓
What ThreadPool-specific reason caused it?For generic reporting, status or result may be enough.
For recovery logic, diagnostics, or error handling, inspect the error code.
Task state and Future state are separate
One submitted operation can involve both:
low-level Task state
+
Future shared stateThey serve different runtime layers.
The worker uses the low-level task state for:
queue lifecycle
execution lifecycle
worker statisticsThe caller uses the Future state for:
asynchronous readiness
value retrieval
exception propagation
ThreadPool error propagationFor ordinary successful work, both layers reach equivalent successful outcomes.
They should still not be treated as the same storage location or the same live state machine.
Use the right view
Use TaskStatus when you need lifecycle information:
const auto status = task.status();Use TaskResult when you need the broad execution outcome:
const auto result = task.result();Use ThreadPoolErrc when you need the ThreadPool-specific reason associated with a Future:
const auto error = future.error();Use Future::get() when the caller simply needs the result and wants failures propagated normally:
const int value = future.get();The model can be summarized as:
Task lifecycle
↓
TaskStatus
Execution outcome
↓
TaskResult
ThreadPool-specific failure
↓
ThreadPoolErrc
Asynchronous value or exception
↓
Future<T>Continue with Scheduling Model for how tasks reach workers, or Errors for the complete ThreadPool error model.