Ch3.16: Herbceptions
Overview
Every page before this one quietly assumed that nothing can fail. From here on nothing is quiet: every error in fast_io is a herbception, and herbceptions are the only error-reporting mechanism in this tutorial.
Two statements are all you have to learn:
throw throws value;— report a failuretry { } catch throws (::std::error e) { }— handle one
You do not need to know anything else to read this page: every example below is a
single int main() that throws an error and catches it again.
1. What a Herbception Is
A herbception is a two-word error value:
struct raw_herbception
{
::std::error_domain_singleton const *domain; // which error code-space
::std::size_t code; // the code inside that space
};
That is sizeof(::std::error) == 2 * sizeof(void *) whatever the error
is. It travels back to the caller in the return registers, next to a
single failure bit in the condition register — CF on x86,
NZCV.C on AArch64. There is no second return path and nothing to set up:
- no stack unwinding, no
.eh_frametables, no personality routines - no heap allocation for the error — ever
- no
std::exceptionbase class, nowhat(), no RTTI
Herbceptions are very, very cheap on both paths. A function that reports errors this way produces the same code as one returning an error code, and the failure path is a plain return of two words. Never choose a “return an error code and check it” style “for speed”, and never avoid reporting a failure because throwing sounds expensive.
2. Building: -fherbceptions -lherbceptions
Herbceptions are a compiler feature, not a library trick, so you need a compiler that implements them. Only our GCC and LLVM builds do:
- GCC — our herbceptions GCC (
g++17+). - Clang — our herbceptions LLVM (
clang++24+). -
MSVC is not supported:
cl.exehas no-fherbceptions, so this tutorial does not build with it. Windows targets are fine — use our Clang or our MinGW GCC.
Two flags, and that is the whole addition:
-
-fherbceptionsturns the language feature on. It must be passed when compiling every translation unit, not only when linking. -
-lherbceptionslinks the runtime, which owns the error domains (the code that names and prints an error).
# our Clang
clang++ --config=$HOME/cfgs/x86_64-linux-gnu-libcxx.cfg \
-O3 -flto=thin -fherbceptions -I$HOME/libraries/fast_io/include \
hello.cc -lherbceptions -o hello
# our GCC
g++ -std=c++26 -O3 -s -fuse-ld=lld -fherbceptions \
-I$HOME/libraries/fast_io/include \
hello.cc -lherbceptions -o hello
On Windows the same flags work with a Windows target, and --sysroot
finds the runtime, so there is no -L to write:
clang++ --config=$HOME/cfgs/x86_64-windows-msvc.cfg \
-O3 -flto=thin -fherbceptions -I$HOME/libraries/fast_io/include \
hello.cc -lherbceptions -o hello.exe
x86_64-w64-mingw32-g++ -std=c++26 -O3 -s -fherbceptions \
-I$HOME/libraries/fast_io/include \
hello.cc -lherbceptions -lntdll -o hello.exe
Forget -fherbceptions and the compiler rejects the syntax; forget
-lherbceptions and the link fails on the domain symbols. Both are
covered in Ch1.8.
3. The throw throws Statement
Reporting a failure is a statement, and it has one shape:
throw throws expression;
throws is the marker saying “this error leaves through the
herbception channel”. The expression is a value of a type that has an error
domain — in practice one of the standard error enums listed in section 7.
#include <fast_io.h>
using namespace ::fast_io::io;
int main()
{
try
{
throw throws ::std::errc::invalid_argument;
}
catch throws (::std::error e)
{
perrln(e);
return 1;
}
return 0;
}
Output, and the exit status the program returns:
[posix](22)Invalid argument
exit status: 1
Three rules, all enforced by the compiler:
-
It has to be somewhere the failure can be handled. Here it is
inside a
trythat has acatch throws. Put it anywhere else and the compiler says:
(Once you write your own functions in Chapter 6, the same statement appears inside them, and they are declared so that the error may leave. Nothing about the statement changes.)error: 'throw throws' is only valid inside a function declared 'throws' or inside a 'try' block with a 'catch throws' handler -
Nothing after it runs. In the example,
return 1;in the handler is what ends the program; the line after thethrow throwsnever executes. -
Rethrowing is
throw throws;— with no operand — and only inside acatch throwshandler. That hands the same error to the enclosing handler:#include <fast_io.h> using namespace ::fast_io::io; int main() { try { try { throw throws ::std::errc::permission_denied; } catch throws (::std::error e) { perr("inner handler: ", e, "\n"); throw throws; // rethrow: pass it to the enclosing handler } } catch throws (::std::error e) { perr("outer handler: ", e, "\n"); return 1; } return 0; }inner handler: [posix](13)Permission denied outer handler: [posix](13)Permission denied
throw throws immediately, before anything has been written or
printed. That is how fast_io itself is written.
4. The try { } catch throws (::std::error e) { } Statement
Handling is a statement too, with exactly one shape:
try
{
/* anything that may fail */
}
catch throws (::std::error e)
{
/* inspect / report / recover / rethrow */
}
The rules, again enforced by the compiler:
-
The handler declares
::std::errorby value. Not by reference, not byconst&, not::std::errc:error: 'catch throws' requires the caught type to be 'std::error' ('std::errc' given) -
You cannot store the error.
::std::errorhas no default constructor and cannot be copied; only the compiler creates one, and the handler owns it. Read what you need inside the handler body. -
The handler may be unnamed:
catch throws (::std::error) { }. Use it when the only thing you can do is continue without the failed operation. -
catch (...)does not catch herbceptions, and it is forbidden in this tutorial: a failure is handled only by acatch throwsblock. -
catch (::std::error e)withoutthrowsis not valid either. It is the legacy handler, and it does not compile:
Legacy C++ exceptions are not part of this tutorial.error: use of deleted function 'std::error::error(const std::error&)'
5. ::std::error: The Members You Use
The value inside the handler is tiny: two words and six things you can ask it. This is the complete list you need.
| Expression | Result | Meaning |
|---|---|---|
e.domain() |
::std::error_domain_singleton const * |
Which code-space the error belongs to. Never null inside a handler. |
e.code() |
::std::size_t |
The raw code inside that domain — for POSIX errors, the errno value. |
e == ::std::errc::permission_denied |
bool |
Exact match: same domain and same code. The type of the value on the right picks the domain. |
e.equivalent(::std::errc::no_such_file_or_directory) |
bool |
Cross-domain match: does this error mean the same thing as that code?
An HTTP 404 equals ENOENT this way.
|
e.to_errc() |
::std::errc |
The POSIX equivalent of the error, as a ::std::errc. |
e.is_code_of<::std::http_errc>() |
bool |
“Was this error produced by this domain?”, ignoring the code. |
#include <fast_io.h>
using namespace ::fast_io::io;
int main()
{
try
{
throw throws ::std::http_errc::not_found;
}
catch throws (::std::error e)
{
println("domain is http: ", e.is_code_of<::std::http_errc>());
println("domain is posix: ", e.is_code_of<::std::errc>());
println("== not_found: ", e == ::std::http_errc::not_found);
println("== errc ENOENT: ", e == ::std::errc::no_such_file_or_directory);
println("equivalent: ", e.equivalent(::std::errc::no_such_file_or_directory));
println("to_errc: ", static_cast<::std::int_least32_t>(e.to_errc()));
println("code: ", e.code());
}
}
domain is http: 1
domain is posix: 0
== not_found: 1
== errc ENOENT: 0
equivalent: 1
to_errc: 2
code: 404
Look at == and equivalent together. The error is
http_errc::not_found, so it is not an ENOENT
(different domain: == is false), but it means the same thing
(so equivalent is true). That difference is the whole reason both
members exist.
6. Printing an Error
Because an error is just a domain and a code, fast_io can print it. The full form is
[domain](code)message, and you can ask for
pieces of it:
#include <fast_io.h>
using namespace ::fast_io::io;
int main()
{
try
{
throw throws ::std::errc::no_space_on_device;
}
catch throws (::std::error e)
{
perrln(e); // the whole line, to stderr
perr("inside a line: ", e, "\n");
perr("domain name: ", ::fast_io::mnp::name(e), "\n");
perr("code + text: ", ::fast_io::mnp::message(e), "\n");
perr("as posix name: ", ::fast_io::mnp::posix_equivalent_name(e), "\n");
perr("as posix text: ", ::fast_io::mnp::posix_equivalent_message(e), "\n");
}
}
[posix](28)No space left on device
inside a line: [posix](28)No space left on device
domain name: posix
code + text: (28)No space left on device
as posix name: posix
as posix text: [posix](28)No space left on device
perrln(e)— the whole thing, on its own line, on stderr.::fast_io::mnp::name(e)— the domain name only.::fast_io::mnp::message(e)— code and text, without the domain name.-
::fast_io::mnp::posix_equivalent_name(e)/::fast_io::mnp::posix_equivalent_message(e)— the same for the POSIX equivalent of a non-POSIX error, so a log line always speakserrc.
perr writes to stderr, print to stdout.
Error reports go to stderr in this tutorial, so a program whose output is redirected
to a file still shows its failures on the terminal. Reporting an error is an ordinary
I/O call: nothing special has to be wrapped around it.
7. The Error Domains
A domain is a code-space. Each one is a type with an error-domain specialization and
its own way of packing code(). These are all of them. You never define a
domain: you pick the one that describes the failure and
throw throws one of its values.
| Value type | Printed tag | code() is |
Describes |
|---|---|---|---|
::std::errc |
[posix] |
the errno value |
POSIX / C errors: invalid_argument,
no_such_file_or_directory, permission_denied,
no_space_on_device, ... The default domain of the library. |
::std::parse_errc |
[parse] |
1 = end of file, 2 = partial, 3 = invalid, 4 = overflow | Text parsing and scanning failures, with no OS involved. |
::std::cmath_errc |
[cmath] |
the FE_* flag bits |
Floating-point exceptions: invalid, divbyzero,
inexact, overflow, underflow. |
::std::http_errc |
[http] |
the HTTP status code | HTTP responses: not_found (404), im_a_teapot (418),
internal_server_error (500), ... |
::std::tls_alert |
[tls] |
(level << 8) | description |
TLS alerts: level (warning/fatal) and description
(certificate_expired, bad_record_mac, ...). |
::std::win32_errc |
[win32] |
the ERROR_* / GetLastError() value |
Windows API failures. Windows targets only. |
::std::nt_errc |
[nt] |
the NTSTATUS / STATUS_* value |
NT native API failures. Windows targets only. |
::std::com_errc |
[com] |
the HRESULT value |
COM failures. Windows targets only. |
::std::wine_errc |
[wine] |
Wine’s host-side errno values |
Host errors reported by Wine’s ntdll. Windows targets
running under Wine. |
::std::exception_ptr |
[itanium] / [msvc] |
an identity for a boxed legacy-EH object | Recognise it and move on: it is what arrives when code that still uses legacy C++ exceptions crosses a herbception boundary. |
Which one to throw? In portable code, ::std::errc. Use a platform or
protocol domain only when the code really came from that platform: a failed
GetLastError() is a win32_errc, a TLS alert from the peer is
a tls_alert, an HTTP status you want to propagate is an
http_errc. Do not translate by hand — equivalent() and
to_errc() do it at the point where somebody needs POSIX semantics.
For example, these three statements are legal and self-explanatory:
throw throws ::std::errc::no_such_file_or_directory;
throw throws ::std::parse_errc::invalid;
throw throws ::std::http_errc::not_found;
8. Nothing Escapes: main Is Where It Stops
A herbception that reaches the top of the program has nobody to be handed to, so the
process traps. On x86-64 the instruction that detects the situation
raises SIGILL, and you get a core dump rather than a message:
$ ./a.out
Illegal instruction (core dumped)
That is deliberate: an unreported error is a bug, and the quickest way to notice a bug is to stop. Two consequences shape every program in this tutorial:
-
maincannot be declaredthrows. The compiler refuses, because there is no caller left to handle the error:error: '::main' cannot be declared with a herbceptions exception specification -
So
maincatches, reports, and turns the failure into an exit status. That is the shape every example in this tutorial has:#include <fast_io.h> using namespace ::fast_io::io; int main() { ::std::int_least32_t value{}; try { scan(::fast_io::c_stdin(), value); println("you typed: ", value); } catch throws (::std::error e) { perrln(e); return 1; } return 0; }$ echo abc | ./a.out [parse](3)Invalid format $ echo 42 | ./a.out you typed: 42
Note which domain answered: the program asked for a number, the input was not one,
and the failure is [parse](3)Invalid format — not an OS error,
because nothing about the operating system went wrong.
9. Where fast_io Fits In
Every operation in fast_io that can fail is declared so that it reports through this
channel, and every operation that cannot fail is declared noexcept. You
never check a return code and you never inspect errno after a call: a
call that fails leaves through the throws path with the original
code-space intact.
That includes I/O, which is why
Ch2.14 wrapped its printing and scanning in
try { } catch throws (::std::error e) { } from the first program on. The
next chapters build on exactly that: files, sockets, scanners, everything reports
failure this way.
10. Common Mistakes
| Mistake | What the compiler says |
|---|---|
throw throws outside any try/handler chain |
'throw throws' is only valid inside a function declared 'throws' or inside a 'try' block with a 'catch throws' handler |
Expecting the line after throw throws to run |
It does not: nothing after the statement executes. |
catch throws (::std::errc e) |
'catch throws' requires the caught type to be 'std::error' ('std::errc' given) |
catch (::std::error e) (legacy handler) |
use of deleted function 'std::error::error(const std::error&)' |
catch (...) { } instead of catch throws |
an unhandled herbceptions failure cannot leave a function without a herbceptions specification |
void f() noexcept throws |
'throws'/'return_failure{E}' cannot be combined with 'noexcept'; the herbceptions specifier supersedes it |
int main() throws |
'::main' cannot be declared with a herbceptions exception specification |
forgetting -fherbceptions |
'throws' has not been declared, at the first throws |
forgetting -lherbceptions |
undefined reference to __cxa_error_domain_* at link time |
Key Takeaways
- All errors in fast_io are herbceptions. There is no error-code API to check and no legacy C++ exception handling in this tutorial.
-
Build with
-fherbceptionson every translation unit and-lherbceptionsat link time, using our GCC or LLVM builds. MSVC cannot do it; on Windows use our Clang or our MinGW GCC. -
Report with
throw throws value;, handle withtry { } catch throws (::std::error e) { }, forward withthrow throws;. -
The handler declares
::std::errorby value, and the error cannot be copied or stored: read what you need inside the handler. -
Ask the error:
domain(),code(),==,equivalent(),to_errc(),is_code_of<T>(). -
Throw
::std::errcin portable code; useparse_errc,cmath_errc,http_errc,tls_alertand the Windows domains when the failure really belongs to them. - A herbception is two words in the return registers next to a condition flag, so it is cheap on both paths. Do not design around its cost.
-
An uncaught herbception traps, and
maincannot bethrows— somaincatches, reports, and returns an exit status.