Ch6.16: Every Function Is noexcept or throws
noexcept (it cannot fail) or throws
(it can report an error to its caller). The single exception in the whole language is
int main(), which has no caller to report to.
Overview
In Ch3.16 you learned to report an error with throw throws and to
handle it with catch throws. Both of those statements are only legal
inside a function that has been marked, and this chapter is about that mark: the
exception specification, which in herbceptions is a two-valued
contract.
int add(int a, int b) noexcept; // cannot fail
int div(int a, int b) throws; // can fail: caller must handle or propagate
There is no third option, and there is no "I did not think about it" option. The
compiler refuses to let a function stay unspecified in a way that leaks failures: a
call to a throws function is only allowed if the caller either handles it
or is itself throws.
1. Why a Function Must Say
Consider the caller. It has two questions about every call:
- Can this fail? If yes, I must handle it or propagate it.
- Can I run it in a context that must not fail? For example inside a destructor, a thread entry point, a signal-like callback, or a function I already promised is
noexcept.
A function declaration answers both, at compile time. That is the entire design: the error contract is part of the type, so the compiler can verify the whole call graph without any documentation, and without any runtime cost.
-
noexcept— "I cannot fail." If you writethrow throwsinside one, or call athrowsfunction without handling it, the program does not compile. -
throws— "I can fail, and the error travels back to you."
throw anything". That freedom is exactly what makes legacy
exceptions unanalysable: any function below you in the call graph can fail, so every
function must be written as if it can. Herbceptions remove the freedom, and with it
the problem.
2. noexcept: I Cannot Fail
A noexcept function promises that it has nothing to report. The promise is
checked by the compiler:
#include <fast_io.h>
using namespace ::fast_io::io;
int square(int x) noexcept
{
return x * x;
}
int bad(int x) noexcept
{
throw throws ::std::errc::invalid_argument; // ERROR: noexcept cannot report
}
int main()
{
println(square(7));
}
The compiler says:
error: 'throw throws' is only allowed inside a function declared 'throws' or 'return_failure{...}'
The same happens for an unhandled call:
::std::uint_least64_t parse_port(int raw) throws; // can fail
void forward_port(int raw) noexcept
{
println(parse_port(raw)); // ERROR: forward_port promised it cannot fail
}
error: call to 'throws' function in a non-'throws' function must be handled by an
enclosing 'try { } catch throws' block, or mark the calling function as 'throws' so
herbceptions can propagate
noexcept functions are everywhere you need a guaranteed-infallible piece
of code: comparison operators, size queries, bit tricks, validation helpers, the body
of a destructor, and any callback whose API cannot deliver an error.
3. throws: I Can Fail, and You Will Know
A throws function reports by throw throws, and the error is
handed to the caller as a ::std::error (see Ch3.16 for every
member function of that value). Nothing about the call site changes: no
out parameter, no error-code return, no errno to check.
#include <fast_io.h>
using namespace ::fast_io::io;
::std::uint_least64_t parse_port(int raw) throws
{
if (raw <= 0 || raw > 65535)
{
throw throws ::std::errc::value_too_large;
}
return static_cast<::std::uint_least64_t>(raw);
}
void print_port(int raw) throws // propagates: no handler here
{
println("port: ", parse_port(raw));
}
int main()
{
try
{
print_port(8080);
print_port(99999); // fails here
}
catch throws (::std::error e)
{
perrln(e);
return 1;
}
}
Output:
port: 8080
[posix](75)Value too large for defined data type
Notice what did not have to happen: parse_port did not return a
sentinel, print_port did not test one, and main did not
inspect an error code after each call. The failure travelled straight from the place
that detected it to the place that decided to report it.
A throws function is the right default for anything that touches the
outside world: opening files, allocating, reading, writing, parsing, connecting. That
is why fast_io marks them all that way.
4. Handling Inside a noexcept Function
A noexcept function may still call fallible code — it just
has to absorb the failure itself. There are two honest ways:
#include <fast_io.h>
using namespace ::fast_io::io;
int read_count(int n) throws
{
if (n < 0)
{
throw throws ::std::errc::invalid_argument;
}
return n * 2;
}
// 1. recover: turn the failure into a value the caller understands
int read_count_or_zero(int n) noexcept
{
try
{
return read_count(n);
}
catch throws (::std::error)
{
return 0;
}
}
// 2. ignore: the failure genuinely cannot be acted on here
void try_log(int n) noexcept
{
try
{
println("count: ", read_count(n));
}
catch throws (::std::error)
{
}
}
int main()
{
println(read_count_or_zero(-1), " ", read_count_or_zero(21));
try_log(-5); // logs nothing, does not crash
}
The second form — the empty named-less handler
catch throws (::std::error) { } — is the only sanctioned way to
swallow a herbception, and it is only correct where reporting is impossible or
pointless. Never swallow to "keep going" in a function that could report.
perrln(e)), the handler itself can fail — printing is I/O
— so the enclosing function cannot be noexcept. Reporting handlers
belong in throws functions (or in main), and that is why the
example above uses the empty form.
5. Constructors and Destructors
Constructors follow the same rule: if construction can fail, the constructor is
throws. A type that owns an OS resource is the classic case —
opening a file really can fail, so the object cannot exist in a half-open state:
struct resource
{
int handle;
resource(int raw) throws // construction may fail
{
if (raw < 0)
{
throw throws ::std::errc::bad_file_descriptor;
}
handle = raw;
}
~resource() // implicitly noexcept
{
}
};
Destructors are implicitly noexcept — make it
explicit if you like (~resource() noexcept), but you cannot opt out. This
is deliberate: a destructor runs while the program is already leaving a scope, and
there is nobody to report to. Consequently a destructor may not call an unhandled
throws function:
struct writer
{
~writer()
{
println("bye"); // ERROR: destructors cannot report failures
}
};
When a cleanup step can fail and the failure matters, do it before the object
dies, in a throws function: call flush() / close()
explicitly and check it there. Letting the destructor do best-effort cleanup is fine;
relying on the destructor for error detection is not.
6. The Specification Is Part of the Type
Because the compiler must be able to reason about it, the specification is part of the function type, exactly like the parameter types. Four consequences you will meet in practice:
You cannot overload on it
int f(int a) noexcept;
int f(int a) throws; // ERROR
error: exception specification in declaration does not match previous declaration
Function pointers carry it
int pure(int a) noexcept { return a + 1; }
int risky(int a) throws { return a + 1; }
int (*p1)(int) noexcept = pure; // ok
int (*p2)(int) throws = risky; // ok
int (*p3)(int) throws = pure; // ERROR: different types
error: cannot initialize a variable of type 'int (*)(int) throws' with an lvalue of
type 'int (int) noexcept': different exception specifications
There is no implicit conversion in either direction: a caller that expects a
throws function must not silently receive a pointer whose contract it
cannot rely on, and vice versa.
Virtual overrides must match
struct base
{
virtual void f() noexcept {}
};
struct derived : base
{
void f() throws override {} // ERROR
};
error: overriding function has a different herbceptions ('throws'/'return_failure{...}')
specifier than the base version, which changes the calling convention
Lambdas and member functions take the spec too
struct point
{
int x, y;
int sum() const noexcept { return x + y; }
};
int main()
{
auto pure_lambda = []() noexcept { return 1; };
auto risky_lambda = []() throws { return 2; };
point p{1, 2};
::fast_io::println(pure_lambda(), risky_lambda(), p.sum(), "\n");
}
7. Conditional Specifications (for Templates)
Inside a template you cannot always say the answer up front: whether your function can
fail depends on the operations on T. Both forms accept a
bool constant expression, and noexcept(expression) answers
"would that expression's call be unable to fail?":
int pure(int a) noexcept { return a + 1; }
int risky(int a) throws { return a; }
// 'noexcept(...)' is itself a compile-time bool
int cond1(int a) noexcept(noexcept(pure(a))) { return pure(a); }
int cond2(int a) throws(!noexcept(risky(a))) { return risky(a); }
noexcept(expression) is the tool you will use constantly when reading
fast_io's own headers: every operation there is annotated with a
conditional specification derived from the operations it performs, so that
println is throws for a real file and stays
noexcept for a sink that cannot fail.
#include <fast_io.h>
using namespace ::fast_io::io;
// ask the library, do not guess
static_assert(!noexcept(println(1))); // I/O can fail
static_assert(!noexcept(scan(::fast_io::c_stdin(),
::std::declval<int&>()))); // input can fail
static_assert(!noexcept(::fast_io::to<::std::uint_least64_t>>("12"))); // parsing can fail
static_assert(noexcept(::fast_io::cstr_len("abc"))); // pure computation
8. main Is the One Exception
main has no caller, so there is nobody to report to. The language
therefore forbids a specification on it:
int main() throws { } // ERROR
error: 'main' cannot be declared with a herbceptions ('throws'/'return_failure{...}')
exception specification; 'main' has no caller able to consume an error result, and
uncaught errors in 'main' trap instead
So main is where the error chain ends: it calls throws
functions inside a try block, prints what went wrong, and returns an exit
code. That is the shape you already saw in Ch2.14:
#include <fast_io.h>
int main()
{
try
{
::fast_io::println("work\n");
}
catch throws (::std::error e)
{
::fast_io::perrln(e);
return 1;
}
return 0;
}
main may also leave a call unhandled in
practice (the compiler treats main as able to propagate), but then a
failure traps with no message. Always catch in main; a trapped process
with no diagnostic is a bug report you have to write yourself.
9. The Rule in One Table
| Situation | What to declare |
|---|---|
| pure computation: arithmetic, comparisons, accessors, bit tricks | noexcept |
| anything that can touch a resource: file, socket, allocation, parse, I/O | throws |
| a helper that reports the failure further up | throws (use throw throws; inside handlers) |
| a function that must not fail but calls fallible code | noexcept + a local handler (recover, or the empty form) |
| destructors |
implicitly noexcept; do explicit flush()/close()
in a throws function when the failure matters
|
| constructors that acquire a resource | throws |
| generic code | noexcept(...)/throws(...) conditional spec |
int main() |
no spec; catch and report |
Key Takeaways
-
Every function except
int mainis eithernoexceptorthrows. Leave nothing unspecified. -
noexceptmeans "cannot fail", and the compiler enforces it: nothrow throwsinside, and no unhandled call to athrowsfunction. -
throwsmeans "can fail"; the caller handles it withcatch throwsor isthrowsitself and propagates. -
A
noexceptfunction that must call fallible code handles it locally: recover with a value, or use the empty handlercatch throws (::std::error) { }when nothing can be done. -
Destructors are implicitly
noexcept; constructors that acquire resources arethrows. - The specification is part of the function type: it cannot be overloaded on, it travels with function pointers, virtual overrides must match it exactly, and lambdas and member functions carry it.
-
Use
noexcept(expression)to ask the compiler (and the library) whether a call can fail;fast_iodeclares all of its I/O asthrows.