Backend Development 28 min read

Exception Handling: Requirements, Modeling, and Best Practices in Backend Development

The article outlines backend exception‑handling best practices, detailing business requirements such as memory‑safe multithreaded throws, clear separation of concerns, framework fallback strategies, simple macro‑based APIs, unified error‑code monitoring, rich debugging information, extensible type‑erased models, and appropriate handling of critical, recoverable, and checked exceptions across development and production environments.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Exception Handling: Requirements, Modeling, and Best Practices in Backend Development

Software development inevitably encounters exceptions; understanding and handling them systematically is crucial for developers. This article, the third part of the "Exception Thought Record" series, focuses on the needs of business development for exception handling and presents several exemplary solutions.

1. Business Development Requirements

1.1 Memory Safety – In multithreaded C++ code the same exception object must be thrown and caught to avoid data races. Example:

static std::atomic
thread_counter;
void foo() {
  thread_local int tid = ++thread_counter;
  try {
    throw MyException("Data from thread", tid);
  } catch (const MyException &ex) {
    assert(ex.tid == tid);
  }
}

When foo is called from multiple threads, C++11 guarantees that the thrown exception and the caught exception refer to the same object, and std::current_exception() provides a thread‑local pointer.

C++20 coroutines embed the exception handling (try/catch) into the generated code, eliminating manual memory‑safety concerns.

1.2 Separation of Concerns – Business code should be concise and only handle errors it can resolve. Errors that need to be propagated should be forwarded with additional context (error code, logs, OSS/monitoring reports). A typical flow includes:

Assert the error condition.

Determine whether it is a primary logic error or a forwarded upstream error.

Set the appropriate error code.

Log the error with context.

Report to OSS or mmdata for monitoring.

Using macros can reduce boilerplate, e.g.:

EXPECT_EQ
CHECK_EQ

Macros can turn expressions into string literals for error messages.

1.3 Framework Fallback Behavior – A well‑designed framework should expose rich error context in development (e.g., call stack, source line) and provide graceful fallback in production (logging and recovery without crashing other services).

Typical fallback strategies:

In development, let the process abort to allow debugging with core dumps.

In production, log the error and continue serving subsequent requests.

1.4 Simplicity of Use – Providing macro‑based helpers such as UCLI_ASSURE_GT allows developers to attach error codes, control codes, messages, and additional resources in a single expression.

static constexpr int fake_result = 1;
UCLI_ASSURE_GT(fake_result, 9)
    << 503
    << UnifiedControlCode::UCC_UNCERTAIN_BUSY
    << "The server is busy, try later"
    << WithRes<MyString>("My Additional Text");

These helpers can be combined with a UnifiedRpcController to safely execute code that may throw:

UnifiedRpcController controller;
int a = controller.SafeCall([](){
  UCLI_ASSURE_EQ(101, 102) << 500
      << UnifiedControlCode::UCC_UNCERTAIN_RETRY
      << "Server down!"
      << WithRes<int>(123456);
  return 100;
});
// controller now contains error_code(), ErrorText(), Options<int>(), etc.

1.5 Operations and Monitoring – Error codes serve as a universal metric for monitoring. A globally unique error‑code system enables rapid detection of abnormal spikes in specific business or component failures.

1.6 Debugging Convenience – Recording call‑frame information at the point of throw allows developers to reconstruct the full error chain later. In production the raw frame pointer can be logged and later symbolized; in debugging builds the framework can resolve the pointer to a readable stack trace.

1.7 Extensibility – Using composition over inheritance, the exception model can be extended with additional data (e.g., custom structs) via type‑erasure. Example:

struct MyString : public std::string {
  using std::string::string;
  std::string ToString() const { return *this; }
};
try {
  static constexpr int fake_result = 1;
  UCLI_ASSURE_GT(fake_result, 9)
      << 503
      << UnifiedControlCode::UCC_UNCERTAIN_BUSY
      << "The server is busy, try later"
      << WithRes<MyString>("My Additional Text");
} catch (const UnifiedException &ex) {
  ex.Res<MyString>();
}

The article also discusses classification of exceptions (Error, RuntimeException, Checked Exception) and appropriate handling strategies for each, emphasizing:

Critical errors (e.g., std::bad_alloc ) should terminate the process.

Recoverable runtime errors ( std::runtime_error and its derivatives) should be caught, logged, and reported.

Checked exceptions ( std::logic_error ) should be transformed into framework‑level error codes and propagated.

Finally, the piece outlines how different environments (debug vs. production) should treat exception information, and provides a roadmap for future articles focusing on higher‑level decision points.

debuggingMonitoringbackend developmentException Handlingframework designC++error modeling
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.