Should You Use Exceptions in Modern C++? A Deep Dive into Consistency, Coroutines, and Real‑World Trade‑offs
This article revisits a 2017 decision to ban exceptions in a large codebase, examines the technical and organizational reasons behind that rule, analyzes exception mechanics, coroutine interactions, and performance impacts with concrete C++ examples, and finally proposes a pragmatic action plan for safely adopting exception‑driven design.
Background and Motivation
In 2017 a business team prohibited the use of exceptions after a technical meeting, aiming to fix issues such as coroutine conflicts, uncaught exceptions crashing worker processes, and data‑consistency problems. The author now revisits that decision as the fourth part of the "Exception Thought Record" series.
Upper‑level Decision Points
Effective software development requires reducing complexity to improve efficiency. Consistency across modeling, code, and runtime is essential for entropy control.
Consistency 1 – Modeling and Implementation Alignment
Exception handling should be designed at the modeling stage, not added ad‑hoc in code. Sequence diagrams must reflect error flows, ensuring that the implemented code matches the design.
Consistency 2 – Component and Framework Responsibility
When using error codes, the same consistency rules must apply to both throwing and catching sides; otherwise, the system loses observability and operational insight.
Consistency 3 – Cross‑language Uniformity
In languages that support exceptions (C++, JavaScript, Java, .NET) the same error‑handling pattern can be used, with only language‑specific syntax differing.
Consistency 4 – Unified Team Thinking
Adopting an "exception mindset" across the organization reduces hidden complexity and improves communication between developers and product owners.
Historical Limitations
Past post‑mortems often produced action plans that ignored changing technical constraints (e.g., libco vs. libcurl incompatibility) and thus became obsolete.
Static Analysis of Throwing Exceptions
Compiling a simple C++ example shows that throw invokes __cxa_allocate_exception to allocate the exception object on a thread‑safe heap, then calls __cxa_throw which starts stack unwinding. The unwind never returns; it either transfers control to a matching catch block or calls std::terminate.
Catching an exception involves __cxa_begin_catch (which adjusts reference counts) and __cxa_end_catch (which destroys the object). The unwind process uses _Unwind_Resume to restore stack frames.
Coroutine Interaction Experiments
Using the libco coroutine library, the author built several benchmark programs to answer two questions:
What happens when an exception is thrown and caught inside a coroutine?
What if a coroutine switch occurs between throw and catch?
Key test cases include:
Throwing an exception and catching it without any coroutine switch.
Throwing, then performing a co_yield_timeout before the catch block.
Using RAII ( BOOST_SCOPE_EXIT_ALL) to defer a coroutine switch until after the exception is caught.
Copy‑capturing the exception object versus reference‑capturing.
Results:
Global variables are unsafe under any multi‑thread or multi‑coroutine scenario.
Thread‑local storage is safe only when no coroutine switch occurs between throw and catch, which is hard to guarantee.
Coroutine‑specific variables provided by libco remain consistent across switches.
Exception objects themselves are safe to pass, but using them after a coroutine switch in the catch block can be unsafe if they were captured by reference; copying the exception resolves the issue.
Re‑analysis of the Original Post‑mortem Conclusions
The earlier conclusions that exceptions caused coroutine conflicts, worker crashes, and data‑consistency loss are revisited. In many cases the root cause was the use of global or thread‑local state rather than the exception mechanism itself. Modern frameworks (e.g., WeChat backend) now provide interceptors and RAII‑style error handling that mitigate those risks.
Safe Exception Usage with libco
Ensure that the catch block does not perform a coroutine switch unless the exception object is copied.
If a switch is required, capture the exception by value.
Prefer coroutine‑specific storage for data that must survive a switch.
Wrap unsafe sections with helper functions (e.g., UnifiedRpcController::SafeCall) that translate exceptions into error‑code objects for the framework.
Action Plan
Promote an "exception‑first" mindset among architects and reviewers.
Incorporate exception handling into domain modeling and sequence diagrams.
Gradually refactor existing services to use RAII and exception propagation instead of scattered error codes.
Introduce code‑review guidelines that require explicit justification ( // NOLINT) for any direct throw usage.
Run the benchmark suite in CI to detect regressions in coroutine safety.
Conclusion
Exceptions, when used consistently and with awareness of coroutine interactions, provide a clean way to model error flows without sacrificing performance or safety. Global state should be avoided, and coroutine‑specific storage combined with value‑captured exceptions yields a robust pattern for modern C++ backend services.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
