C++ Exceptions at RTOS Task Boundaries: Catch Failures Before They Cross a C Entry Point
Put a complete exception-to-outcome boundary inside the C++ task wrapper, before control can escape into the RTOS entry machinery. Construct task-owned objects inside that protected region, finish their cleanup through normal C++ control flow, and only then report the outcome and enter the kernel’s approved terminal path. Kernel task deletion is not a substitute for C++ stack unwinding.
This guide uses C++17 with GCC 14.2.0 and the FreeRTOS Kernel V11.1.0 task-entry contract. Executed tests cover an original C++ host wrapper; no FreeRTOS task or MCU unwinder was executed. ISR exceptions, asynchronous hardware-fault conversion and cross-task exception transport are outside scope.
Draw the actual call boundary
Mark the task entry, application worker, constructors, callbacks into C libraries and error-reporting functions. For each edge, record language linkage, exception specification and the compiler/runtime settings needed if unwinding could cross it. A callback reached through a C library deserves its own boundary review even when an outer C++ handler exists.
extern "C" specifies language linkage; it does not itself install an exception handler or universally prohibit unwinding through every C frame. The GCC 14.2.0 manual explains that C code interoperating with C++ handlers may need exception support enabled. The reliable integration policy here is to contain exceptions before the RTOS boundary, rather than depend on unqualified unwinding through kernel or port code.

Include construction, not only the work loop
This original pattern was compiled and tested with GCC 14.2.0 in C++17 mode:
Outcome run_guarded(int mode) noexcept {
try {
Worker worker(mode);
worker.run(mode);
return Outcome::ok;
} catch (const DomainError&) {
return Outcome::domain;
} catch (...) {
return Outcome::unexpected;
}
}
The fixture’s Outcome values are a small enum; handlers perform no allocation, formatting or blocking operation. Placing Worker inside the try block allows a constructor exception to reach the same translation boundary. Constructing it before the try block would leave that failure outside the intended containment region.
The public WG21 N4659 C++17-era working draft, section 18.2, describes destruction while transferring to a handler. When construction fails, already constructed subobjects require cleanup; the complete object’s destructor is not a replacement for that partial-construction behavior. Resource acquisition should therefore be represented by members or local objects whose own cleanup is valid at each construction stage.
Understand what noexcept does and does not do
The wrapper is noexcept because its intended result channel is an Outcome, not an escaping exception. That declaration does not catch a missed exception. If exception propagation reaches a nonthrowing function boundary, termination is required under the language rules. N4659 sections 18.4 and 18.5.1 also prevent treating termination as a guarantee that every local object was unwound first.
Audit destructors, catch handlers and reporting functions accordingly. A destructor that throws during exception cleanup can defeat the recovery path. Marking a complicated reporter noexcept merely changes its failure endpoint; it does not prove that its internals cannot throw, allocate or block. Prefer a previously qualified, bounded result channel whose failure behavior is explicit.
Keep the kernel terminal operation outside owned scopes
The V11.1.0 task API requires a task entry not to return. It also distinguishes task deletion from freeing application-owned memory. Arrange the entry wrapper so run_guarded returns after its local Worker has been destroyed, then translate the outcome into the application’s supervised-stop, restart or failure policy.
If that policy uses self-deletion, perform it only after all relevant C++ automatic objects and application resources have completed their intended cleanup. Calling vTaskDelete from inside an owning C++ scope does not create a normal return or throw that invokes those destructors. Likewise, externally deleting a task is not an exception-based cancellation mechanism.
An outcome report proves only its declared application milestone. It is not, by itself, evidence that the kernel has finished deleting the task or that another task may immediately reuse its storage. Keep that lifetime protocol separate from the exception translation.
Use constructor and worker failure probes
The executed host fixture counted live resource objects and complete Worker destructor calls. Normal completion returned ok and left zero live resources. A constructor exception returned domain, cleaned the already constructed resource member and invoked no complete Worker destructor. A worker exception returned domain and destroyed the completed Worker. An unrelated thrown integer returned unexpected through the catch-all path.
A separate negative process allowed an exception to escape a noexcept function. Its installed test terminate handler exited with code 86, which was observed. This controlled test confirms the selected host path only; it does not claim a firmware reset policy, target unwind-table correctness or cleanup before termination.
Qualify the target build before relying on recovery
- Confirm that the actual C++ objects use exception-enabled compilation and a compatible runtime.
- Inspect linker placement and retention of the target’s required unwind metadata and personality/runtime support.
- Check every precompiled library and callback boundary on a throwing path.
- Inject failures after each resource-acquisition stage and verify the intended cleanup order.
- Exercise failure reporting when its normal dependency is unavailable; avoid recursive logging or allocation.
- Measure target stack and response-time effects rather than borrowing desktop results.
For a hypothetical calibration worker, a constructor failure might mean “configuration rejected,” while a worker failure after hardware activation might require a separate safe-stop operation before reporting. Catching the exception does not establish that a peripheral operation was reversed. The outcome contract must describe the real resource state.
Accept the integration when every supported throw path ends in a defined outcome or an explicitly qualified terminal policy, with no exception escaping the approved boundary. Keep compiler support, C++ cleanup and RTOS lifecycle evidence distinct so a successful catch cannot hide an unfinished task or resource obligation.