Exception Handling

2 posts

lineOriginal article

Code Quality Improvement Techniques Part (opens in new tab)

When implementing resource management patterns similar to Kotlin's `use` or Java's try-with-resources, developers often face the challenge of handling exceptions that occur during both primary execution and resource cleanup. Simply wrapping these multiple failures in a custom exception container can inadvertently break the calling code's error-handling logic by masking the original exception type. To maintain code quality, developers should prioritize the primary execution exception and utilize the `addSuppressed` mechanism to preserve secondary errors without disrupting the expected flow. ### The Risks of Custom Exception Wrapping Creating a new exception class to consolidate multiple errors during resource management can lead to significant issues for the caller. * Wrapping an expected exception, such as an `IOException`, inside a custom `DisposableException` prevents specific `catch` blocks from identifying and handling the original error. * This pattern often results in unhandled exceptions or the loss of specific error context, especially when the wrapper is hidden inside utility functions. * While this approach aims to be "neat" by capturing all possible failures, it forces the caller to understand the internal wrapping logic of the utility rather than the business logic errors. ### Prioritizing Primary Logic over Cleanup When errors occur in both the main execution block and the cleanup (e.g., `dispose()` or `close()`), it is critical to determine which exception takes precedence. * The exception from the main execution block is typically the "primary" failure that reflects a business logic or IO error, whereas a cleanup failure is often secondary. * Throwing a cleanup exception while discarding the primary error makes debugging difficult, as the root cause of the initial failure is lost. * In a typical `try-finally` block, if the `finally` block throws an exception, it naturally suppresses any exception thrown in the `try` block unless handled manually. ### Implementing Better Suppression Logic A more robust implementation mimics the behavior of Kotlin’s `Closeable.use` by ensuring the most relevant error is thrown while keeping others accessible for debugging. * Instead of creating a wrapper class, use `Throwable.addSuppressed()` to attach the cleanup exception to the primary exception. * If only the primary block fails, throw that exception directly to satisfy the caller's `catch` requirements. * If both the primary block and the cleanup fail, throw the primary exception and add the cleanup exception as a suppressed error. * If only the cleanup fails, it is then appropriate to throw the cleanup exception as the standalone failure. ### Considerations for Checked and Unchecked Exceptions The impact of exception handling varies by language, particularly in Java where checked exceptions are enforced by the compiler. * Converting a checked exception into an unchecked `RuntimeException` inside a wrapper can cause the compiler to miss necessary error-handling requirements. * If exceptions have parent-child relationships, such as `IOException` and `Exception`, wrapping can cause a specific handler to be bypassed in favor of a more generic one. * It is generally recommended to only wrap checked exceptions in `RuntimeException` when the error is truly unrecoverable and the caller is not expected to handle it. When designing custom resource management utilities, always evaluate which exception is most critical for the caller to see. Prioritize the primary execution error and use suppression for auxiliary cleanup failures to ensure that your error-handling remains transparent and predictable for the rest of the application.

datadog3 min readCurated summary

.NET Continuous Profiler: Exception and lock contention

Datadog’s .NET continuous profiler can diagnose performance problems that CPU and wall-time profiling may miss: excessive exceptions and lock contention. Exceptions consume significant CPU and latency, while locks increase request latency through waiting rather than active computation. By collecting exception details and measuring contention duration with low overhead, the profiler helps developers identify the code and runtime conditions responsible. ## Exception Profiling - The CLR notifies the profiler through `ICorProfilerCallback::ExceptionThrown`, providing the exception’s `ObjectID`. - `ExceptionProvider::OnExceptionThrown` extracts details such as: - Exception type - Thread ID - Source location - The profiler maps the exception object to its `ClassID` using `ICorProfilerInfo::GetClassFromObject`. - Type names are resolved and cached by the `FrameStore`. - Exception messages require reading the private `System.Exception._message` field: - The profiler locates `System.Exception` in `mscorlib` or `System.Private.CoreLib`. - `GetModuleMetaData` provides access to assembly metadata. - `FindTypeDefByName` locates the type definition. - `GetClassFromTokenAndTypeArgs` obtains its `ClassID`. - `GetClassLayout` identifies field offsets. - `FindField` locates `_message`. - `GetStringLayout2` provides the string buffer and length needed to read the message. - Collecting exception counts by type, message, and call site makes it possible to replace expensive exception-driven control flow with cheaper checks such as `TryParse`. ## Lock Contention Monitoring - Standard .NET monitoring exposes contention counts, but generally not how long threads waited or where the contention originated. - The CLR emits: - `ContentionStart` when a thread begins waiting - `ContentionStop` when it acquires the lock - On .NET Framework, contention duration is calculated from timestamps recorded for each thread because `ContentionStop` does not include the duration. - Since .NET 8, `ContentionStart` includes the lock’s `ObjectID` and the ID of the thread holding it, allowing the profiler to identify the blocking thread. - .NET Framework exposes counters such as `Contention Rate / Sec` and `Total # of Contentions`; .NET Core provides `monitor-lock-contention-count` through `dotnet-counters`. - These counters alone do not reveal the duration or cause of waits. ## Consuming CLR Events - Since .NET 5, profilers can synchronously receive CLR events through `ICorProfilerCallback10::EventPipeEventDelivered`. - Datadog’s `ClrEventParser` interprets event payloads based on event IDs and keywords. - The parsed duration is passed to `ContentionProvider::OnContention`. - Runtime differences require version-specific handling because event payloads are not identical across .NET Framework and .NET Core. The practical recommendation is to profile both exception frequency and lock-wait duration, rather than relying only on CPU usage or contention counters. This reveals inefficient exception-based logic and identifies locks—and, on newer runtimes, the threads holding them—that materially affect application latency.

Read original(opens in new tab)