MISRA C++ Exception Handling
About 1583 wordsAbout 5 min
2026-03-25
Overview
Chapter 15 of MISRA C++:2008 governs exceptions. In safety-critical embedded systems, exceptions introduce:
- Non-deterministic control flow — timing cannot be guaranteed
- Hidden code paths — every
throwsite and stack unwinding path must be certified - Code size overhead — exception tables add ROM/RAM cost
- RTTI dependency — stack unwinding requires RTTI in some implementations
- Conflict with real-time constraints — exception handling may violate RTOS timing guarantees
Many safety-critical projects disable exceptions entirely (-fno-exceptions in GCC/Clang) and rely on error return codes or status enums instead.
MISRA C++:2008 Exception Rules (Chapter 15)
Rule 15-0-1 — No Exceptions Across Module or Subsystem Boundaries (Document)
Exceptions shall only be used for error handling and shall not be used as a means of normal control flow.
Key principle: An exception must represent an exceptional, unrecoverable condition — not expected error states.
// NON-COMPLIANT: exception for expected condition (not truly exceptional)
uint32_t divide(uint32_t a, uint32_t b) {
if (b == 0U) {
throw std::range_error("division by zero"); // Non-compliant — expected case
}
return a / b;
}
// COMPLIANT: return an error code for expected failure cases
enum class DivResult { Ok, DivByZero };
DivResult divide(uint32_t a, uint32_t b, uint32_t& result) {
if (b == 0U) {
result = 0U;
return DivResult::DivByZero; // Compliant — explicit error code
}
result = a / b;
return DivResult::Ok;
}Rule 15-0-2 — An Exception Object Shall Not Be an Object of a Class with Virtual Members (Advisory)
Exception objects with virtual members carry vtable pointers and interact with RTTI in complex ways during unwinding.
// NON-COMPLIANT (advisory): exception type with virtual method
class SensorError : public std::runtime_error {
public:
explicit SensorError(const char* msg) : std::runtime_error{msg} {}
virtual const char* sensor_name() const { return "unknown"; } // Virtual — non-compliant
};
// COMPLIANT: no virtual methods on exception types
struct SensorError {
uint32_t error_code;
const char* message;
};
throw SensorError{0x01U, "ADC timeout"};Rule 15-0-3 — Handlers of a Function-try-block Implementation Not Used (Document)
A function-try-block on a constructor or destructor shall only use rethrowing.
Rule 15-1-1 — The throw Expression Shall Only Throw Objects of a Type Derived from std::exception (Advisory)
// NON-COMPLIANT (advisory): throwing a plain integer
throw 42; // Non-compliant — not derived from std::exception
// NON-COMPLIANT (advisory): throwing a raw string
throw "error occurred"; // Non-compliant
// COMPLIANT
throw std::runtime_error{"Sensor failure"}; // Compliant — derived from std::exceptionRule 15-1-2 — NULL Shall Not Be Thrown (Required)
A NULL pointer shall not be thrown.
// NON-COMPLIANT
throw NULL; // Non-compliant — throws null pointer
// NON-COMPLIANT
const char* msg = nullptr;
throw msg; // Non-compliant
// COMPLIANT
throw std::runtime_error{"Error"};Rule 15-1-3 — Empty Throw Shall Only Occur in a Catch Handler (Required)
A rethrow (throw;) expression shall only be used within a compound statement enclosed by a catch handler.
// NON-COMPLIANT: throw; outside a catch handler
void bad_rethrow() {
throw; // Non-compliant — undefined behaviour outside a catch block
}
// COMPLIANT
void handle_error() {
try {
do_work();
}
catch (const std::exception& e) {
log_error(e.what());
throw; // Compliant — rethrow within catch handler
}
}Rule 15-3-1 — Exceptions Shall Be Caught by Reference (Required)
Exceptions shall be raised by value and caught by (const) reference.
// NON-COMPLIANT: catch by value — copy-constructs, slices polymorphic exceptions
try {
do_work();
}
catch (std::exception e) { // Non-compliant — caught by value (slicing)
log_error(e.what());
}
// NON-COMPLIANT: catch by pointer
catch (std::exception* e) { // Non-compliant — pointer catch
log_error(e->what());
}
// COMPLIANT: catch by const reference
try {
do_work();
}
catch (const std::exception& e) { // Compliant
log_error(e.what());
}Rule 15-3-2 — Where Multiple Handlers Are Present, at Least One Is Catch-All (Advisory)
When multiple catch clauses are specified for a single try block, the last catch clause must be a catch(...).
// NON-COMPLIANT (advisory): no catch-all
try {
do_work();
}
catch (const NetworkError& e) { ... } // Misses all other exception types
catch (const SensorError& e) { ... } // Non-compliant — no catch(...)
// COMPLIANT
try {
do_work();
}
catch (const NetworkError& e) { handle_network(e); }
catch (const SensorError& e) { handle_sensor(e); }
catch (...) {
log_fatal("Unexpected exception");
// MUST rethrow or terminate — do not swallow unknown exceptions in safety code
throw;
}Rule 15-3-3 — Handlers of a Derived Class Shall Appear Before Handlers of Base (Required)
When catching a hierarchy, place derived-class handlers before base-class handlers.
class BaseError : public std::exception { };
class DerivedError : public BaseError { };
// NON-COMPLIANT: base before derived — DerivedError is always caught by BaseError handler
try {
do_work();
}
catch (const BaseError& e) { ... } // Non-compliant — catches derived too; derived handler unreachable
catch (const DerivedError& e) { ... } // Unreachable
// COMPLIANT: specific before general
try {
do_work();
}
catch (const DerivedError& e) { ... } // Compliant — more specific first
catch (const BaseError& e) { ... } // Catches remaining BaseError variants
catch (...) { throw; } // Catch-allRule 15-3-4 — Each Exception Explicitly Thrown Shall Have a Handler (Required)
Each exception that may be thrown shall have a handler — either in the current function or documented as propagating to the caller.
Rule 15-3-5 — A Class Type Exception Shall Always Have a Handler of a Base Class Type (Required)
If a class exception is thrown, there should be a handler for its base class too.
Rule 15-3-6 — catch(...) Shall Not Be Used in Applications Where All Exceptions Are Handled at a Single Level (Required)
Rule 15-3-7 — Where Multiple Handlers Are Used, Only the First Matching Handler Is Executed (Document)
This is a documentation reminder: C++ executes only the first matching catch clause. Handlers must be ordered from most to least derived.
Rule 15-4-1 — Dynamic Exception Specifications Shall Not Be Used (Required)
throw() exception specifications (deprecated in C++11, removed in C++17) shall not be used.
// NON-COMPLIANT: dynamic exception specification (deprecated)
void process() throw(std::runtime_error); // Non-compliant
// NON-COMPLIANT: empty throw spec (pre-C++11 noexcept equivalent)
void process() throw(); // Non-compliant
// COMPLIANT: use noexcept (C++11+)
void process() noexcept; // Compliant
void process(); // Compliant — no specificationRule 15-5-1 — A Class Destructor Shall Not Exit via an Exception (Required)
// NON-COMPLIANT: destructor can throw
class Buffer {
public:
~Buffer() {
if (m_data != nullptr) {
flush_data(); // Non-compliant — flush_data() might throw
}
}
};
// COMPLIANT: destructor catches and swallows (or logs) any exception
class Buffer {
public:
~Buffer() noexcept {
try {
if (m_data != nullptr) {
flush_data();
}
}
catch (...) {
// Log or signal error — do NOT rethrow from destructor
signal_flush_error();
}
}
};Why: If a destructor throws during stack unwinding from another exception,
std::terminate()is called — the program terminates in an uncontrolled manner.
Rule 15-5-2 — Where a Function's Declaration Includes an Exception Specification, the Function Shall Only Be Capable of Throwing Exceptions of the Indicated Type(s) (Required)
This rule is enforced automatically by noexcept and checked at link time in modern C++.
Rule 15-5-3 — The Terminate() Function Shall Not Be Called Implicitly (Required)
Code shall not create a situation where std::terminate() is called implicitly. This includes:
- Throwing from a
noexceptfunction - Throwing from a destructor during unwinding
- Not catching a thrown exception at all
// NON-COMPLIANT: noexcept function propagates exception — calls terminate()
void critical_process() noexcept {
do_work(); // Non-compliant — if do_work() throws, terminate() is called
}
// COMPLIANT
void critical_process() noexcept {
try {
do_work();
}
catch (...) {
handle_error_safely(); // Compliant — no exception escapes noexcept
}
}Disabling Exceptions in Safety-Critical Embedded Systems
Many safety standards (ISO 26262, IEC 61508) require deterministic, predictable execution. Exception handling is often disabled:
GCC/Clang: -fno-exceptions
# In CMakeLists.txt
target_compile_options(my_firmware PRIVATE -fno-exceptions -fno-rtti)MISRA-compliant alternatives to exceptions:
// Pattern 1: Error return codes
enum class Status { Ok, InvalidParam, HardwareFault, Timeout };
Status init_sensor(Sensor& s) {
if (!s.is_connected()) { return Status::HardwareFault; }
s.configure();
return Status::Ok;
}
// Pattern 2: std::optional (C++17) for nullable return values
std::optional<uint32_t> read_adc(uint8_t channel) {
if (channel >= ADC_MAX_CHANNELS) { return std::nullopt; }
return adc_read_raw(channel);
}
// Pattern 3: Result type (custom)
template<typename T, typename E>
struct Result {
bool ok;
T value;
E error;
};
Result<uint32_t, Status> measure_temperature() {
uint32_t raw;
if (!adc_read(TEMP_CHANNEL, raw)) {
return {false, 0U, Status::HardwareFault};
}
return {true, convert_to_celsius(raw), Status::Ok};
}noexcept Specification Guide
| Function Type | noexcept Specification |
|---|---|
| Destructors | Always noexcept (default in C++11+) |
| Move constructors | noexcept where possible (enables optimisations) |
| Move assignment | noexcept where possible |
swap | noexcept |
| Simple getters | noexcept |
| Hardware I/O that can fail | noexcept (with internal error handling) |
| System init functions | noexcept (return status code) |
Summary: Exception Handling Rules
| Rule | Level | Summary |
|---|---|---|
| 15-0-1 | Document | Exceptions only for truly exceptional conditions |
| 15-0-2 | Advisory | Exception object shall not have virtual members |
| 15-1-1 | Advisory | Throw types derived from std::exception |
| 15-1-2 | Required | Do not throw NULL |
| 15-1-3 | Required | throw; only inside a catch handler |
| 15-3-1 | Required | Catch by reference, throw by value |
| 15-3-2 | Advisory | Last handler shall be catch(...) |
| 15-3-3 | Required | Derived-class handlers before base-class handlers |
| 15-3-4 | Required | Every thrown exception must have a handler |
| 15-4-1 | Required | No dynamic exception specifications (throw()) |
| 15-5-1 | Required | Destructors shall not throw |
| 15-5-3 | Required | No implicit std::terminate() |
| — | Best practice | Consider -fno-exceptions for safety-critical targets |
| — | Best practice | Use error return codes or std::optional instead |