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

Errors

Vix Async uses standard C++ error mechanisms.

Operational failures are generally reported through std::system_error, carrying either a Vix Async std::error_code or an error code produced by the underlying system or networking backend.

Task exceptions continue through normal coroutine exception propagation, while detached execution has no caller available to receive them.

Use the public Vix Async header:

cpp
#include <vix/async.hpp>

For examples that print output:

cpp
#include <vix/print.hpp>

The Async error API lives in:

cpp
vix::async::core

Async error codes

Vix-specific runtime conditions are represented by:

cpp
vix::async::core::errc

The current enumeration is:

cpp
enum class errc : std::uint8_t
{
  ok = 0,

  invalid_argument,
  not_ready,
  timeout,
  canceled,
  closed,
  overflow,

  stopped,
  queue_full,

  rejected,

  not_supported
};

These codes provide a common vocabulary for errors originating from the Async runtime itself.

Not every public operation currently emits every value in this enumeration. Some codes define runtime-level error conditions that are available for Async components as the module evolves.

std::error_code

Convert an Async error to std::error_code with:

cpp
auto error = make_error_code(
  errc::canceled
);

errc is registered as a standard C++ error-code enum, so it integrates with the normal <system_error> model.

The Async category is named:

text
async

For example:

cpp
std::error_code error = make_error_code(
  errc::stopped
);

vix::print(
  error.category().name(),
  error.message()
);

The category identifies the error as originating from Vix Async rather than an operating-system or networking category.

Error messages

The Async category currently maps its codes to these messages:

ErrorMessage
errc::okok
errc::invalid_argumentinvalid argument
errc::not_readynot ready
errc::timeouttimeout
errc::canceledcanceled
errc::closedclosed
errc::overflowoverflow
errc::stoppedstopped
errc::queue_fullqueue full
errc::rejectedrejected
errc::not_supportednot supported

Use the error code rather than comparing message strings.

Prefer:

cpp
if (error.code() == make_error_code(errc::stopped))
{
  // ...
}

instead of:

cpp
if (error.what() == std::string{"stopped"})
{
  // ...
}

The code expresses the actual error condition.

std::system_error

Async operations that need to report an operational failure generally throw:

cpp
std::system_error

through their task result.

For example:

cpp
task<void> run(io_context& ctx)
{
  try
  {
    co_await ctx.timers().sleep_for(
      std::chrono::seconds(5)
    );
  }
  catch (const std::system_error& error)
  {
    vix::print(
      "error:",
      error.what()
    );
  }
}

Because the exception crosses the normal coroutine await boundary, it can be handled with ordinary C++ try and catch.

Inspect the error code

Use:

cpp
error.code()

when the application needs to distinguish one condition from another.

cpp
catch (const std::system_error& error)
{
  if (error.code() == make_error_code(errc::stopped))
  {
    vix::print("runtime stopped");
    co_return;
  }

  throw;
}

This is preferable to treating every asynchronous interruption as the same failure.

Cancellation

Application cancellation uses:

cpp
errc::canceled

The cancellation API also provides:

cpp
cancelled_ec()

as a convenience helper.

For example:

cpp
catch (const std::system_error& error)
{
  if (error.code() == cancelled_ec())
  {
    vix::print("operation canceled");
    co_return;
  }

  throw;
}

Cancellation means that application code requested the operation to stop.

It is different from runtime shutdown.

Runtime stop

When an Async service is being shut down, a suspended operation can report:

cpp
errc::stopped

For example, a timer wait can leave suspension during context shutdown:

text
sleep_for pending

timer service stops

coroutine resumes

errc::stopped

Code that expects runtime teardown can treat this as a normal lifecycle condition.

cpp
catch (const std::system_error& error)
{
  if (error.code() == make_error_code(errc::stopped))
  {
    co_return;
  }

  throw;
}

Cancellation and shutdown should remain distinct

These conditions answer different questions.

text
errc::canceled

the application no longer wants this operation

errc::stopped

the Async service needed by the operation is stopping

Combining both into one generic "operation failed" path can hide useful lifecycle information.

For example:

cpp
catch (const std::system_error& error)
{
  if (error.code() == cancelled_ec())
  {
    vix::print("request canceled");
    co_return;
  }

  if (error.code() == make_error_code(errc::stopped))
  {
    vix::print("runtime shutting down");
    co_return;
  }

  throw;
}

Rejected CPU work

The thread pool reports:

cpp
errc::rejected

when a submit() operation cannot be accepted because the pool has already stopped.

cpp
ctx.cpu_pool().stop();

try
{
  int value = co_await ctx.cpu_pool().submit([](){
    return 42;
  });
}
catch (const std::system_error& error)
{
  if (error.code() == make_error_code(errc::rejected))
  {
    vix::print("CPU work rejected");
  }
}

A rejected coroutine submission completes with an error rather than remaining suspended indefinitely.

post() uses a different interface and reports acceptance with bool:

cpp
bool accepted = ctx.cpu_pool().post([](){
  perform_work();
});

if (!accepted)
{
  vix::print("CPU work not accepted");
}

Signal errors

The signal service uses several Async error codes.

A second concurrent waiter reports:

cpp
errc::not_ready

An invalid non-positive signal passed to add() reports:

cpp
errc::invalid_argument

A platform without the required signal-wait implementation reports:

cpp
errc::not_supported

Cancellation and service shutdown use:

cpp
errc::canceled
errc::stopped

For example:

cpp
try
{
  int signal = co_await ctx.signals().async_wait();

  vix::print("signal:", signal);
}
catch (const std::system_error& error)
{
  if (error.code() == make_error_code(errc::not_ready))
  {
    vix::print("another waiter is active");
    co_return;
  }

  throw;
}

Network errors

TCP, UDP, and DNS operations can fail for reasons that come from the operating system or Asio backend.

Examples include:

  • connection refused
  • address already in use
  • host not found
  • connection reset
  • socket closed by the peer
  • invalid network address
  • other system networking failures

These failures retain their underlying std::error_code.

They are not converted into an Async errc merely to make every error belong to the same category.

This preserves information provided by the networking stack.

Distinguish Async and system errors

A network operation may therefore produce either:

text
Async error

category = "async"

or

system / Asio error

platform-specific error category

You can inspect the category when necessary:

cpp
catch (const std::system_error& error)
{
  vix::print(
    "category:",
    error.code().category().name()
  );

  vix::print(
    "message:",
    error.code().message()
  );
}

Application logic should generally check specific codes it understands and propagate or report everything else.

Do not erase useful network errors

Avoid converting every network exception into a generic application failure immediately:

cpp
catch (...)
{
  throw std::runtime_error("network failed");
}

That loses the original error code.

When possible, keep the original std::system_error available:

cpp
catch (const std::system_error& error)
{
  vix::print(
    "network error:",
    error.code(),
    error.what()
  );

  throw;
}

The caller can then decide how a particular error should affect the application.

Task exceptions

task<T> is not limited to std::system_error.

Any exception thrown by task code can propagate to the coroutine that awaits it.

cpp
task<int> compute()
{
  throw std::runtime_error(
    "computation failed"
  );

  co_return 0;
}

The parent can handle it normally:

cpp
try
{
  int value = co_await compute();

  vix::print("value:", value);
}
catch (const std::runtime_error& error)
{
  vix::print(
    "computation error:",
    error.what()
  );
}

std::system_error is therefore the mechanism for many operational Async failures, not a restriction on which exceptions a task can carry.

Exceptions from CPU submissions

Exceptions thrown by a callable passed to submit() are captured on the worker thread and rethrown when the awaiting coroutine resumes.

cpp
try
{
  int value = co_await ctx.cpu_pool().submit([]() -> int {
    throw std::runtime_error("failed");
  });
}
catch (const std::runtime_error& error)
{
  vix::print(error.what());
}

The worker thread does not terminate because the callable threw.

The exception remains part of the observable submit() result.

when_all errors

when_all starts all supplied tasks and waits for all of them to complete.

If one or more tasks throw:

text
start every task

task A throws
task B continues
task C continues

all tasks complete

first captured exception rethrown

This means catching the when_all exception does not imply that the other tasks were abandoned when the first error occurred.

cpp
try
{
  auto results = co_await when_all(
    ctx.get_scheduler(),
    first(),
    second()
  );
}
catch (const std::exception& error)
{
  vix::print(
    "composition failed:",
    error.what()
  );
}

when_any errors

when_any is based on the first task to complete, not the first task to succeed.

If the first completion is an exception:

text
task A throws first

A wins completion race

when_any resumes

A exception rethrown

The losing tasks continue running.

A later successful task does not replace the failed winner.

This behavior matters when when_any is used for races or fallback strategies.

Detached task exceptions

A task started with:

cpp
spawn_detached(
  ctx,
  background()
);

has no awaiting parent.

There is therefore no coroutine to receive an exception that escapes background().

The detached boundary consumes that exception.

text
detached task

exception escapes

detached boundary

exception consumed

If a detached failure matters, handle it inside the task:

cpp
task<void> background()
{
  try
  {
    co_await perform_work();
  }
  catch (const std::exception& error)
  {
    vix::print(
      "background error:",
      error.what()
    );
  }
}

Detached execution should only be used when giving up caller-side error propagation is intentional.

Scheduler callback exceptions

Ordinary callbacks posted to the scheduler also have no result channel.

If one throws:

cpp
ctx.post([](){
  throw std::runtime_error("failed");
});

the scheduler consumes the exception and continues processing its event loop.

This prevents one fire-and-forget callback from terminating all scheduler execution.

If the caller needs to observe failure, use a task or another result-bearing operation instead.

Thread-pool post() exceptions

The same principle applies to:

cpp
ctx.cpu_pool().post([](){
  throw std::runtime_error("failed");
});

A posted worker callable has no awaiting coroutine.

Its exception is consumed by the worker boundary so the worker thread can continue processing later jobs.

Use submit() when exception propagation is required.

Cancellation callback exceptions

Cancellation callbacks registered with:

cpp
token.on_cancel([](){
  perform_cancellation();
});

are also invoked through a no-result callback boundary.

If a cancellation callback throws, the cancellation state consumes that exception.

request_cancel() remains noexcept.

Cancellation callbacks should therefore perform small, reliable operations needed to stop or wake pending work.

Accessing services after shutdown

io_context service access has a different failure mechanism.

After:

cpp
ctx.shutdown();

attempting to create or access a lazy service such as:

cpp
ctx.timers();
ctx.cpu_pool();
ctx.signals();
ctx.net();

throws:

cpp
std::runtime_error

with the context-shutdown condition.

This is a programming and lifecycle misuse rather than the asynchronous completion of a pending operation.

That distinction is why it does not use errc::stopped.

Pending operations interrupted by service shutdown report errc::stopped.

Attempting to use the context after shutdown throws std::runtime_error.

Error boundaries

It is useful to recognize where errors remain observable and where execution becomes detached.

OperationFailure observation
co_await task<T>Exception propagates to awaiting coroutine.
thread_pool::submit()Callable exception propagates to awaiting coroutine.
Timer/network/signal awaitstd::system_error propagates to awaiting coroutine.
when_allFirst captured task exception is rethrown after all tasks complete.
when_anyWinning task exception is rethrown.
spawn_detached()Escaping task exception is consumed.
scheduler::post() callbackEscaping callback exception is consumed.
thread_pool::post()Escaping worker exception is consumed.
cancellation callbackEscaping callback exception is consumed.
service access after io_context::shutdown()std::runtime_error is thrown directly.

This distinction helps determine where application error handling should live.

Handle expected conditions narrowly

A useful pattern is to handle the conditions that are part of normal control flow and propagate everything else.

cpp
try
{
  co_await operation();
}
catch (const std::system_error& error)
{
  if (error.code() == cancelled_ec())
  {
    co_return;
  }

  if (error.code() == make_error_code(errc::stopped))
  {
    co_return;
  }

  throw;
}

This keeps unexpected network, operating-system, and application failures visible.

Catching all exceptions and silently continuing can hide real faults.

Async error reference

The complete current errc set is:

CodeMeaning
errc::okNo error.
errc::invalid_argumentAn API received an invalid argument.
errc::not_readyThe operation cannot proceed in the current state.
errc::timeoutAn operation timed out.
errc::canceledApplication cancellation was requested.
errc::closedA resource or channel is closed.
errc::overflowA capacity or numeric limit was exceeded.
errc::stoppedA runtime or service has stopped.
errc::queue_fullAn internal task queue is full.
errc::rejectedA submission was rejected.
errc::not_supportedThe operation is unavailable on the platform.

Again, the existence of a code in errc does not mean every Async API currently emits it.

API overview

The core error API is:

cpp
enum class errc : std::uint8_t;

Convert an Async code with:

cpp
std::error_code make_error_code(
  errc error
) noexcept;

Access the Async error category with:

cpp
const std::error_category&
category() noexcept;

The category name is:

text
async

Cancellation also provides:

cpp
std::error_code cancelled_ec() noexcept;

which is equivalent to:

cpp
make_error_code(errc::canceled)

Next step

Continue with CMake for the build and linking requirements of the Async module.

Then read:

Released under the MIT License.