Skip to main content

Preserving State on Partial Success with Custom Exceptions

Capture partial success data in custom exceptions for smarter error handling

Updated over a week ago

Overview

In operations that involve a series of related steps, such as creating a customer and then creating multiple associated addresses, there's a risk of failure at any point. When an error occurs midway through the process, the system can be left in a partially complete state. This document presents a robust error-handling strategy that uses a custom ExpectedException to wrap any exception that occurs. This custom exception is designed to carry the data that was successfully created before the error, giving the calling system a clear picture of the operation's final state and enabling more intelligent recovery actions.

Problem/Challenge

When a multi-step operation fails, standard exception handling often reports only the point of failure, not the successful transactions that preceded it. For instance, if an operation to create a customer and ten associated addresses fails on the third address, the first two successfully created addresses become "orphaned" without a clear way for the calling system to know they exist. This lack of information leads to several challenges:

  • Inconsistent System State: The system is left in an inconsistent state where some data is created, and some is not, with no programmatic visibility into what succeeded.

  • Data Loss or Duplication: Without knowing what was partially successful, retrying the entire operation could lead to duplicate records.

  • Complex Recovery: Manual intervention is often required to identify and clean up orphaned records, increasing operational overhead.

Solution Design

The core of this error-handling pattern is to treat exceptions not just as errors, but as events that can contain valuable state information. The solution involves the following steps:

  • Iterative Processing: The logic iterates through a collection of items (e.g., addresses), attempting to create and link each one individually within a try-catch block.

  • Stateful Exception: When an operation within the try block fails, the generic Exception is caught. Instead of simply re-throwing the original exception, the code instantiates a custom ExpectedException.

  • Encapsulating Partial Success: This new exception is populated with the original error message and, crucially, the output object. This object contains all the data that was successfully processed and saved before the point of failure.

  • Propagating Rich Information: This new, state-aware exception is then thrown. This allows the calling method to catch an error that contains not only the reason for the failure but also the results of the partial success.

Benefits

  • State-Aware Errors: The system that calls this code receives not just a failure message, but also the data that was successfully processed, enabling more intelligent error handling.

  • Informed Recovery: With the partial success data, the calling system can decide whether to roll back the created records, log the partial success for manual intervention, or notify a user with specific details about what was and was not completed.

  • Improved System Robustness: This approach prevents the system from being left in an unknown or inconsistent state by explicitly capturing and reporting on partial failures.

  • Reduced Manual Cleanup: By providing the context of what succeeded, the need for manual database cleanup and reconciliation is significantly reduced.

Implementation Notes

  • Custom Exception Design: The ExpectedException class should be designed to accept the original exception or its message, as well as a property to hold the successfully saved data (e.g., SavedExternalData).

  • Idempotency: For operations that might be retried, consider designing the creation logic to be "idempotent," or safe to run multiple times without creating duplicate entries. This can work in conjunction with the partial success data to gracefully complete the remaining steps.

  • Logging: It is crucial to log the full details of the ExpectedException, including the partial data, to provide a clear audit trail for debugging and manual review if necessary.

Code pattern for handling expected exceptions

/** Example implementation of expected exception*/
// Iterate through each address in the list of addresses
foreach (var address in Addresses)
{
try
{
// Create the address as a new customer record. This is an asynchronous operation.
var newAddress = (Customer)await address.Create(activeCallWrapper);
// Add the newly created address to the output object's list of addresses.
output.Addresses.Add(newAddress);
// An address can be both a ship-to and bill-to address.
// If the address is marked as a ship-to address
if (newAddress.IsShip.Value)
// Link it to the parent customer as a shipping address.
await LinkCustomerAsAddress(activeCallWrapper, output.Id.Value, newAddress, true);

// If the address is marked as a bill-to address
if (newAddress.IsBill.Value)
// Link it to the parent customer as a billing address.
await LinkCustomerAsAddress(activeCallWrapper, output.Id.Value, newAddress, false);
}
// If any kind of exception occurs during the 'try' block
catch(Exception ex)
{
// Wrap the original exception message in a custom 'ExpectedException'.
// This new exception also carries the 'output' object, which contains all the data
// that was successfully saved before the error occurred.
throw new ExpectedException(ex.Message) { SavedExternalData = output };
}
}
// Return the final customer object with all its addresses.
return output;

.

Did this answer your question?