1. noexcept as a contract and the cost of violation
When you first see noexcept, it's tempting to read it as “this function definitely doesn't fail”. Unfortunately, no: it can fail as much as it likes, it just has no right to report that to the outside via an exception. It's like a “no smoking” sign: it doesn't guarantee there will be no smoke, it guarantees that if smoke appears — there will be consequences (and in C++ those consequences are usually quick and loud).
Notation like:
int f() noexcept;
means: if something inside f() throws an exception, it must not cross the function boundary. And here is the key point: the function boundary is the most important concept.
Inside you may call code that can potentially throw, but then you are obliged to catch the exception inside f() and finish the function “by the rules” (for example, return a fallback value, write an error to the log, set a flag, etc.). If an exception escapes — the language's standard reaction is immediate program termination.
Keep this in mind as a flow:
flowchart TD
A[Inside the function] --> B{Did an exception occur?}
B -- no --> C[Normal return]
B -- yes --> D{Is the function noexcept?}
D -- no --> E[Exception propagates up the stack]
D -- yes --> F["std::terminate (abnormal termination)"]
So noexcept is not “super armor against errors”, but a strict promise: “I will not report errors via exceptions to the outside”.
What happens on violation: std::terminate
If an exception leaves a noexcept function, the standard behavior is to call std::terminate. That means the program ends immediately, and there will be no “I'll do a little more work and save the file”.
Minimal “what not to do” example:
#include <stdexcept>
void bad() noexcept {
throw std::runtime_error("boom");
}
int main() {
bad(); // program will terminate via std::terminate
}
Why is C++ so strict? Because noexcept is a contract other code may rely on. For example, the standard library and your own code can build algorithms that assume “no safety net” if you promised no exceptions. Breaking that promise can break not only your function but also the logic outside it, so the language chooses the most predictable outcome: terminate.
Can you make a noexcept function that calls operations that may throw? Yes. But then you must place a “barrier” — catch everything inside and not let it escape. For example, this is sometimes done in destructors, because destructors should be as safe as possible from the outside:
#include <iostream>
struct Cleaner {
~Cleaner() noexcept {
try {
std::cout << "cleanup\n";
} catch (...) {
// even if something went wrong, do not let it escape
}
}
};
This does not mean “catch everything and stay silent” — it means “I must fulfill the noexcept contract, so I do not let exceptions out”. What to do inside the catch (log, set a flag, ignore) depends on project policy, but the very existence of a barrier here is justified by the contract.
2. Where noexcept is usually appropriate
It's tempting to add noexcept everywhere to be “faster” and “cooler”. Don't do that: it's like putting an “exit here” sign on every door and then wondering why people crash into walls. Use noexcept where you can realistically guarantee that no exceptions will escape.
Let's collect typical candidates in one table. This is not scripture, but a good starter map.
| Category | Example | Why appropriate |
|---|---|---|
| Simple field getters | |
Return a value, no allocations, no validation |
| Checks / predicates | |
Usually just comparison / size check |
| swap for a class | |
swap is often used as a commit operation and should be predictable |
| Destructors | (by intent) |
A destructor should not throw outside, otherwise stack unwinding becomes chaotic |
| Small utilities without allocations | |
Pure arithmetic / comparison |
Important nuance: “simple field getters” are indeed good candidates. For example:
#include <string>
struct Task {
int id{};
std::string title;
bool done{false};
bool is_done() const noexcept { return done; }
};
is_done() does nothing that could throw, and such functions are pleasant to mark noexcept: it highlights that the call is safe and you don't expect surprises.
swap is a bit more interesting: swap is often used as a technical basis for exception safety (for example, in a commit/rollback pattern). If swap can unexpectedly throw, the whole idea of a “short commit step” becomes less reliable.
3. Where marking noexcept is dangerous
There is a whole class of functions that you cannot honestly promise to be noexcept, because by their nature they may encounter an error, and throwing is a normal way to report it.
If you put noexcept where real errors can occur, you turn “error → exception → handling” into “error → terminate”. You didn't remove the error; you just removed the ability to handle it.
Typical dangerous zones are operations that involve memory allocations and container growth, parsing strings to numbers, building large strings, and validators that are supposed to throw invalid_argument on bad input.
Compare two variants with a small parsing example:
#include <string>
#include <stdexcept>
int parse_id(const std::string& s) { // NOT noexcept
if (s.empty()) {
throw std::invalid_argument("empty id");
}
return std::stoi(s); // may throw
}
If we wrote int parse_id(...) noexcept, then any bad input would turn into abnormal termination. That's bad UX even for a console utility, and for a library — almost a crime against users.
Another example: a method that adds an element to std::vector is almost always potentially throwing (at least because of allocation on growth). Even if “on average” everything will be fine, you have no right to promise noexcept unless you control the situation.
#include <string>
#include <vector>
struct TaskBook {
std::vector<std::string> titles;
void add(std::string t) { // NOT noexcept
titles.push_back(std::move(t)); // may allocate
}
};
That doesn't mean such functions are “bad”. It means their contract should not forbid exceptions if exceptions are a normal part of operation.
4. noexcept(expr) and conditional noexcept
One of the most practical parts of the topic is the operator noexcept(expr). It is evaluated at compile time and answers the question: “can this expression throw an exception?” The result is true or false. This is useful when you want not to rely on faith but to formally check code properties.
Let's start with a simple example:
static int add(int a, int b) noexcept { return a + b; }
static_assert(noexcept(add(1, 2)));
static_assert here acts as a “gatekeeper”: if someone changes add so that it ceases to be noexcept, the project simply won't compile. This is useful in large codebases where changes sometimes arrive like a snowball.
The most interesting part starts when you use noexcept(expr) for a conditional noexcept: “my function is noexcept if and only if what I call inside is noexcept”. This lets you write more honest wrappers.
For example, for swap you can make it noexcept if swapping all fields is also noexcept:
#include <utility>
#include <vector>
struct Box {
std::vector<int> data;
void swap(Box& other) noexcept(noexcept(data.swap(other.data))) {
data.swap(other.data);
}
};
Yes, the notation looks a bit “template-like”, but its meaning is straightforward: we don't invent a promise, we derive it from the promises of the components. That's much more honest than slapping noexcept on by guesswork.
5. Example: hardening TaskBook
To make the topic concrete, let's embed it into a small demo project. Suppose we have a console app TaskBook — a small task manager. We store tasks in a std::vector, assign ids, and can add tasks. Such “learning” apps nicely show where noexcept helps and where it is dangerous.
Start with the task model:
#include <string>
struct Task {
int id{};
std::string title;
bool done{false};
bool is_done() const noexcept { return done; }
};
Here is_done() is an honest candidate for noexcept: it simply reads a bool.
Now implement the storage TaskBook. We want swap to be as predictable as possible, and noexcept is appropriate here:
#include <utility>
#include <vector>
class TaskBook {
public:
void swap(TaskBook& other) noexcept {
tasks_.swap(other.tasks_);
std::swap(next_id_, other.next_id_);
}
private:
std::vector<Task> tasks_;
int next_id_{1};
};
Why does noexcept look reasonable here? We don't perform manual allocations, don't validate input, don't parse strings or call stoi. We simply swap two vectors and two integers.
Now add a copy-and-swap style assignment operator. Important: the operator= itself is not required to be noexcept. It can throw while creating the parameter other (copying may allocate), and that's fine. But if copying fails, the left-hand object is unchanged — that's the strong guarantee.
#include <utility>
class TaskBook {
public:
void swap(TaskBook& other) noexcept;
TaskBook& operator=(TaskBook other) { // NOT noexcept
swap(other);
return *this;
}
};
Here is a nice separation of concerns: the potentially risky part (copying) happens before entering the body, and the commit is done via swap, which we marked noexcept. The logic is easy to inspect.
And the add_task method we explicitly do not mark noexcept, because it can grow a std::vector and allocate memory:
#include <string>
#include <utility>
#include <vector>
class TaskBook {
public:
void add_task(std::string title) { // NOT noexcept
tasks_.push_back(Task{next_id_, std::move(title), false});
++next_id_;
}
private:
std::vector<Task> tasks_;
int next_id_{1};
};
If you ask: “what if I still mark it noexcept because my app is tiny?” — one day it won't be tiny. Then instead of handling an error you will get std::terminate.
6. Common mistakes when using noexcept
Mistake #1: adding noexcept as a “speed-up” without understanding it's a contract.
A beginner often treats noexcept like constexpr: add it and everything gets faster and cooler. In practice noexcept is a promise that, if broken, causes the program to terminate via std::terminate. If you cannot prove that no exception will escape, don't promise it.
Mistake #2: marking functions that handle input, parsing, and validation as noexcept.
Parsing numbers, analyzing strings, checking user data — these are places where errors are normal. If you forbid exceptions from escaping, any “bad string” becomes abnormal termination. That's not “reliability”, that's “self-destruction on encountering a space”.
Mistake #3: thinking noexcept automatically provides the strong guarantee.
noexcept only says whether an exception will escape. It says nothing about “state immutability”. You can write a noexcept function that changes half the fields, then catches an exception inside and returns “somehow”. From the noexcept contract point of view it's satisfied, while logically the object may end up in a strange state. Thus noexcept and exception safety are different axes and should be considered separately.
Mistake #4: making a function noexcept and forgetting that it calls potentially throwing code.
This often happens after refactoring: yesterday the function was a simple getter, you marked it noexcept, and tomorrow you add operations with std::string, push_back, or stoi. The compiler doesn't always forbid that directly, because a throw can happen only at runtime. The result is an unexpected std::terminate in the worst place. Fixes: either remove noexcept, build an internal barrier and meaningful handling, or use noexcept(expr) and static_assert if it's critical.
Mistake #5: “catching everything” inside a noexcept function and silently continuing as if nothing happened.
Sometimes people learn about std::terminate and start sprinkling try/catch (...) { } everywhere to avoid crashes. Formally the noexcept contract is fulfilled, but you risk hiding the real cause and leaving the system in an unpredictable state. If you place a barrier inside noexcept, do it consciously: either guarantee a correct state, or finish the operation predictably (e.g., return an error code or set a flag), not “pretend nothing happened”.
GO TO FULL VERSION