Vix.cpp v2.8.5 is here Read the blog
Skip to content

Errors

The ThreadPool module uses several complementary mechanisms to report failure:

text
ThreadPoolErrc
std::error_code
std::system_error
user exceptions
TaskStatus
TaskResult
bool submission results

The mechanism depends on the API being used.

For result-producing work:

cpp
auto future = pool.submit([](){
  return 42;
});

the Future exposes:

cpp
future.status();
future.result();
future.error();

and:

cpp
future.get();

either returns the value or throws the stored failure.

Error model

The high-level model is:

text
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_error

These cases should be handled differently when the application needs to distinguish them.

ThreadPoolErrc

ThreadPool-specific error codes are represented by:

cpp
vix::threadpool::ThreadPoolErrc

The available values are:

cpp
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

ErrorValueMessage
ok0ok
invalid_argument1invalid argument
stopped2thread pool stopped
rejected3task rejected
queue_full4task queue full
timeout5operation timed out
cancelled6operation cancelled
not_ready7operation not ready
not_supported8operation not supported
internal_error9internal thread pool error

ok

cpp
vix::threadpool::ThreadPoolErrc::ok

means no ThreadPool error is stored.

A successfully completed Future normally reports:

text
status = completed
result = success
error  = ok

For example:

cpp
auto future = pool.submit([](){
  return 42;
});

const int value = future.get();

after successful completion:

cpp
future.error() == vix::threadpool::ThreadPoolErrc::ok;

invalid_argument

cpp
vix::threadpool::ThreadPoolErrc::invalid_argument

represents an invalid API argument.

When stored in a Future shared state, it maps to:

text
TaskStatus::failed
TaskResult::failure

The 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

cpp
vix::threadpool::ThreadPoolErrc::stopped

represents an operation associated with a stopped pool or executor.

When stored in a Future, it maps to:

text
TaskStatus::rejected
TaskResult::rejected

The 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:

cpp
vix::threadpool::ThreadPoolErrc::rejected

for that path.

rejected

cpp
vix::threadpool::ThreadPoolErrc::rejected

is the main high-level error produced when submit() or handle() cannot submit work.

For example:

cpp
vix::threadpool::ThreadPool pool(1);

pool.shutdown();

auto future = pool.submit([](){
  return 42;
});

The Future is immediately ready with:

text
status = rejected
result = rejected
error  = rejected

Calling:

cpp
future.get();

throws:

cpp
std::system_error

whose error code is ThreadPoolErrc::rejected.

queue_full

cpp
vix::threadpool::ThreadPoolErrc::queue_full

represents queue-capacity rejection.

When stored in a Future, it maps to:

text
TaskStatus::rejected
TaskResult::rejected

The 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:

text
worker queue rejects task

Scheduler::submit() returns false

ThreadPool::submit()

Future error = rejected

Therefore a Future currently reports:

cpp
ThreadPoolErrc::rejected

rather than:

cpp
ThreadPoolErrc::queue_full

for this path.

See Queue and Rejection Policies.

timeout

cpp
vix::threadpool::ThreadPoolErrc::timeout

maps to:

text
TaskStatus::timed_out
TaskResult::timeout

A common high-level path is an expired deadline before the submitted callable begins:

cpp
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:

text
error  = timeout
status = timed_out
result = timeout

and get() throws std::system_error.

Execution timeout has a current distinction

An execution timeout configured through:

cpp
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:

cpp
auto future = pool.submit([](){
  std::this_thread::sleep_for(
    std::chrono::milliseconds{10}
  );

  return 42;
}, options);

can currently produce:

text
Future:
  status = completed
  result = success
  error  = ok
  value  = 42

low-level worker task:
  result = timeout

Therefore runtime timeout metrics can increase while the corresponding Future still reports success.

See Timeouts.

cancelled

cpp
vix::threadpool::ThreadPoolErrc::cancelled

maps to:

text
TaskStatus::cancelled
TaskResult::cancelled

For example, a handle cancelled before its callable begins can produce:

cpp
auto handle = pool.handle([](){
  return 42;
});

handle.cancel();

If cancellation is observed before the user callable starts, the Future state becomes cancelled.

Calling:

cpp
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

cpp
vix::threadpool::ThreadPoolErrc::not_ready

represents an operation that cannot complete yet.

It is also the value returned by:

cpp
future.error();

for an invalid Future with no shared state.

For example:

cpp
vix::threadpool::Future<int> future;

const auto error = future.error();

gives:

text
ThreadPoolErrc::not_ready

However, 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:

cpp
vix::threadpool::Future<int> future;

reports:

text
valid()  false
ready()  false
status() created
result() none
error()  not_ready

Calling:

cpp
future.get();

does not throw a ThreadPool std::system_error.

It throws:

cpp
std::future_error

with:

text
std::future_errc::no_state

The same applies to:

cpp
future.wait();
future.wait_for(...);
future.wait_until(...);

because those operations require a valid shared state.

not_supported

cpp
vix::threadpool::ThreadPoolErrc::not_supported

represents an unsupported operation.

When stored in a Future, it maps to:

text
TaskStatus::failed
TaskResult::failure

The current high-level ThreadPool task-submission path does not publish this error itself.

It remains part of the public error-code API.

internal_error

cpp
vix::threadpool::ThreadPoolErrc::internal_error

maps to:

text
TaskStatus::failed
TaskResult::failure

It 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:

cpp
std::error_category

available through:

cpp
vix::threadpool::threadpool_category();

Its name is:

text
vix.threadpool

For example:

cpp
const auto& category =
  vix::threadpool::threadpool_category();

const char* name = category.name();

name points to:

text
vix.threadpool

Convert to std::error_code

Use:

cpp
std::error_code error =
  vix::threadpool::make_error_code(
    vix::threadpool::ThreadPoolErrc::timeout
  );

The result contains:

text
category = vix.threadpool
value    = 5
message  = operation timed out

Implicit std::error_code conversion

ThreadPoolErrc is registered as:

cpp
std::is_error_code_enum<
  vix::threadpool::ThreadPoolErrc
>

so this is also valid:

cpp
std::error_code error =
  vix::threadpool::ThreadPoolErrc::cancelled;

The resulting error code uses the ThreadPool category automatically.

Inspect an error code

For example:

cpp
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:

text
category: vix.threadpool
value: 4
message: task queue full

Helper functions

Use:

cpp
vix::threadpool::is_ok(error);

to check:

text
error == ThreadPoolErrc::ok

For example:

cpp
if (vix::threadpool::is_ok(future.error()))
{
  // No ThreadPool error is stored.
}

Use:

cpp
vix::threadpool::is_error(error);

for the opposite check:

text
error != ThreadPoolErrc::ok

These 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:

text
value
exception
ThreadPoolErrc

Conceptually:

text
SharedState<T>
├── optional value
├── exception_ptr
├── ThreadPoolErrc
├── TaskStatus
├── TaskResult
└── ready flag

The first successful attempt to make the state ready wins.

Later completion attempts are ignored.

Successful value

When a Promise stores:

cpp
promise.set_value(42);

the shared state becomes:

text
ready  = true
error  = ok
status = completed
result = success
value  = 42

future.get() returns the stored value.

Successful void completion

For:

cpp
vix::threadpool::Promise<void> promise;
auto future = promise.get_future();

promise.set_value();

the Future becomes:

text
ready  = true
error  = ok
status = completed
result = success

and:

cpp
future.get();

returns normally.

Stored ThreadPool error

When:

cpp
promise.set_error(
  vix::threadpool::ThreadPoolErrc::cancelled
);

the shared state becomes ready with a mapped status and result.

Then:

cpp
future.get();

throws:

cpp
std::system_error

using:

cpp
make_error_code(ThreadPoolErrc::cancelled)

Error-to-status mapping

When SharedState::set_error() is used, the mappings are:

ThreadPoolErrcTaskStatusTaskResult
okcompletedsuccess
cancelledcancelledcancelled
timeouttimed_outtimeout
rejectedrejectedrejected
queue_fullrejectedrejected
stoppedrejectedrejected
invalid_argumentfailedfailure
not_readyfailedfailure
not_supportedfailedfailure
internal_errorfailedfailure

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:

text
Why did the asynchronous operation fail?

TaskResult describes the broader outcome:

text
How did the task finish?

For example:

text
ThreadPoolErrc::queue_full
ThreadPoolErrc::stopped
ThreadPoolErrc::rejected

all map to:

cpp
TaskResult::rejected

The result groups several specific error reasons into one execution outcome.

ThreadPoolErrc and TaskStatus are also different

TaskStatus describes lifecycle state:

text
created
queued
running
completed
failed
cancelled
timed_out
rejected

ThreadPoolErrc provides an error reason.

For example:

text
error = timeout

status = timed_out

while:

text
error = queue_full

status = rejected

User exceptions

If a submitted callable throws:

cpp
auto future = pool.submit([]() -> int {
  throw std::runtime_error{"task failed"};
});

the wrapper catches the exception and stores its:

cpp
std::exception_ptr

in the shared state.

The state becomes:

text
status = failed
result = failure
error  = internal_error

but the original exception is retained separately.

Original user exception is rethrown

Calling:

cpp
future.get();

on that Future rethrows:

cpp
std::runtime_error{"task failed"}

It does not throw:

cpp
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:

text
ready

already retrieved?

stored exception?
  ├── yes → rethrow original exception

  └── no

ThreadPool error?
  ├── yes → throw std::system_error

  └── no

return value

error() after a user exception

Even though get() rethrows the original user exception:

cpp
future.error();

reports:

cpp
vix::threadpool::ThreadPoolErrc::internal_error

because set_exception() sets the error field to internal_error.

Therefore application code can observe:

text
status = failed
result = failure
error  = internal_error

while get() still preserves the original exception type.

Catch user exceptions separately

For example:

cpp
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:

text
exception thrown by user callable

from:

text
ThreadPoolErrc stored by asynchronous infrastructure

when those categories matter.

Inspect std::system_error

A ThreadPool infrastructure error can be handled as:

cpp
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:

cpp
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:

cpp
threadpool_category();

Submission errors from post()

ThreadPool::post() does not return a Future or error code.

Its error channel is:

cpp
bool

For example:

cpp
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:

text
pool stopped
queue rejected task
invalid empty callable
other scheduler rejection

They all appear as:

text
false

when 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:

cpp
vix::threadpool::Executor::Task task;

const bool accepted = pool.post(task);

returns:

text
false

No 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:

text
submit()

cannot accept

Promise::set_error(rejected)

Future immediately ready

The caller can inspect:

cpp
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:

cpp
auto handle = pool.handle([](){
  return 42;
});

Inspect:

cpp
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:

text
valid task ID
valid Future
CancellationSource

Therefore:

cpp
handle.valid()

does not mean:

text
task was accepted by the ThreadPool

Inspect its asynchronous result.

Cancellation before submission

For submit(), a task option can already contain a cancelled token:

cpp
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:

text
error  = cancelled
status = cancelled
result = cancelled

without constructing a worker task for execution.

Cancellation precedence over deadline

The current pre-run mapping is:

cpp
mergedOptions.cancellation.cancelled()
  ? ThreadPoolErrc::cancelled
  : ThreadPoolErrc::timeout

Therefore when:

text
cancellation already requested
and
deadline already expired

the Future receives:

cpp
ThreadPoolErrc::cancelled

at 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:

text
Future error  = timeout
Future status = timed_out
Future result = timeout

The 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:

text
callable begins

cancellation requested

callable ignores cancellation

returns 42

Future stores value 42

can result in:

text
status = completed
result = success
error  = ok

even though:

cpp
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:

cpp
const auto status = future.wait_for(
  std::chrono::milliseconds{10}
);

can return:

cpp
std::future_status::timeout

This means only:

text
the caller waited 10 ms
and
the Future was not ready yet

It does not:

text
set Future error to ThreadPoolErrc::timeout
cancel the task
change TaskStatus to timed_out
change TaskResult to timeout

The Future continues running normally.

wait_until() has the same distinction

Similarly:

cpp
const auto status = future.wait_until(deadline);

returning:

cpp
std::future_status::timeout

only describes the caller-side wait operation.

It is independent from:

text
TaskOptions timeout
TaskOptions deadline
ThreadPoolErrc::timeout
TaskStatus::timed_out
TaskResult::timeout

Future retrieval errors

Future::get() can also throw standard Future errors unrelated to ThreadPoolErrc.

The main cases are:

text
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:

cpp
auto future = pool.submit([](){
  return 42;
});

const int first = future.get();

A second:

cpp
future.get();

throws:

cpp
std::future_error

with:

text
std::future_errc::future_already_retrieved

The Future object can still report its stored:

text
status
result
error

after retrieval, but the value cannot be retrieved a second time.

Promise retrieval errors

A Promise can produce its Future only once:

cpp
vix::threadpool::Promise<int> promise;

auto future = promise.get_future();

Calling:

cpp
promise.get_future();

again throws:

cpp
std::future_error{
  std::future_errc::future_already_retrieved
}

A moved-from Promise with no state also throws:

cpp
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:

cpp
vix::threadpool::Promise<int> promise;
auto future = promise.get_future();

promise.set_error(
  vix::threadpool::ThreadPoolErrc::not_supported
);

The Future becomes:

text
status = failed
result = failure
error  = not_supported

and:

cpp
future.get();

throws std::system_error.

Promise exception completion

Use:

cpp
promise.set_exception(
  std::make_exception_ptr(
    std::runtime_error{"failure"}
  )
);

or inside a catch block:

cpp
promise.set_current_exception();

The Future becomes:

text
status = failed
result = failure
error  = internal_error

while get() rethrows the original captured exception.

First completion wins

SharedState completion methods ignore calls made after the state is already ready.

For example:

cpp
promise.set_value(42);

promise.set_error(
  vix::threadpool::ThreadPoolErrc::cancelled
);

the second operation does not replace the successful value.

The result remains:

text
value  = 42
status = completed
result = success
error  = ok

This 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:

cpp
promise.set_value(value);

or:

cpp
promise.emplace_value(...);

Do not use:

cpp
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:

cpp
std::future_errc::broken_promise

when an unresolved producer disappears.

This differs from a behavior developers may expect from std::promise.

For example, a queued wrapper removed by:

cpp
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:

text
submit()

Future returned

task waiting in queue

pool.clear()

task wrapper destroyed

producer disappears

Future still non-ready

Calling:

cpp
future.get();

can then block indefinitely.

See Lifecycle and Shutdown.

Non-draining shutdown has the same risk

With:

cpp
config.drain_on_shutdown = false;

a queued result-producing task may never execute before workers stop.

Its Future is not automatically converted into:

text
rejected
cancelled
broken_promise

and 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:

text
stopped
rejected
queue_full

but high-level ThreadPool::submit() currently reduces scheduler submission failure to:

cpp
ThreadPoolErrc::rejected

The same is true for handle().

Therefore application code currently cannot use a returned Future to distinguish:

text
queue full
from
scheduler rejection
from
completed pool shutdown

through 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:

cpp
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:

cpp
ThreadPoolErrc::internal_error

through error(), while the exact exception type is available only by calling get() and catching it.

Error handling example

cpp
#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

cpp
#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:

text
category = vix.threadpool
value    = 3
message  = task rejected

Error paths by API

APIFailure reporting
ThreadPool::post()false
ThreadPool::submit()Future
ThreadPool::handle()TaskHandle containing Future
Future::get() user exceptionRethrows original exception
Future::get() ThreadPool errorThrows std::system_error
Invalid Future operationThrows std::future_error
Future::wait_for() caller timeoutReturns std::future_status::timeout
Promise::set_error()Stores ThreadPoolErrc
Promise::set_exception()Stores std::exception_ptr
Low-level TaskTaskStatus and TaskResult
Runtime observationMetrics and statistics

Handling errors by intent

When the operation is fire-and-forget:

cpp
if (!pool.post(task))
{
  handle_submission_failure();
}

When a value is required:

cpp
try
{
  auto value = future.get();
}
catch (const std::system_error& error)
{
  // ThreadPool error.
}
catch (...)
{
  // User callable exception.
}

When cancellation matters:

cpp
if (handle.error() ==
    vix::threadpool::ThreadPoolErrc::cancelled)
{
  handle_cancelled();
}

When only readiness should be bounded:

cpp
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:

text
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
               error

The important properties are:

  • ThreadPoolErrc is the module's public error-code enum.
  • The current values range from ok = 0 through internal_error = 9.
  • ThreadPool error codes use the vix.threadpool error category.
  • ThreadPoolErrc converts to std::error_code.
  • make_error_code() can be used explicitly.
  • is_ok() and is_error() provide simple enum checks.
  • SharedState maps ThreadPool errors to TaskStatus and TaskResult.
  • cancelled maps to cancelled status/result.
  • timeout maps to timed-out status/result.
  • rejected, queue_full, and stopped all map to rejected status/result.
  • invalid_argument, not_ready, not_supported, and internal_error map to failed/failure.
  • Future::get() throws std::system_error for 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_error into std::system_error.
  • post() exposes only a boolean submission result.
  • The high-level submit() and handle() APIs currently report scheduler submission failures as ThreadPoolErrc::rejected.
  • They do not currently preserve queue_full or stopped as separate Future errors.
  • An invalid Future reports not_ready through error(), but operations such as get() throw std::future_error(no_state).
  • get() can be called only once and later retrieval throws std::future_error(future_already_retrieved).
  • wait_for() and wait_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 successful Promise<T> completion, not set_error(ok).
  • The custom Promise does not currently produce automatic broken_promise completion 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.

Released under the MIT License.