A plugin that fails silently, or fails with a confusing raw .NET error, is hard to support. This chapter covers failing cleanly, and the tool you'll reach for constantly while building and debugging plugins: the tracing service.
- Throw InvalidPluginExecutionException to block an operation with a readable message
- ITracingService writes diagnostic messages to the plugin trace log
- Depth on the execution context tracks how many plugins deep the current call chain is
- The platform enforces a maximum depth to stop runaway loops
The incident report before the explanation
A good security guard doesn't just slam the door on a visitor — they calmly explain why the visitor can't proceed, and file an incident report for later review. A plugin should do the same: trace what's happening for later debugging, then fail with a message the user can actually understand.
InvalidPluginExecutionException
Throwing a raw, unhandled .NET exception from a plugin produces a generic, unhelpful error for the end user. Throwing InvalidPluginExecutionException instead lets you supply a clear, specific message that surfaces directly in the app — the standard way to cleanly block an operation.
try
{
if (opportunity.Contains("statecode") &&
((OptionSetValue)opportunity["statecode"]).Value == 1)
{
tracingService.Trace("Blocking delete: Opportunity {0} is Won.", opportunity.Id);
throw new InvalidPluginExecutionException(
"Won opportunities can't be deleted. Deactivate it instead.");
}
}
catch (Exception ex) when (!(ex is InvalidPluginExecutionException))
{
tracingService.Trace("Unexpected error: {0}", ex.Message);
throw new InvalidPluginExecutionException("An unexpected error occurred. Please contact support.");
}
ITracingService: your debugging window
Call tracingService.Trace(...) at meaningful points in your code — before a risky operation, after retrieving data, inside a catch block. Those messages land in the plugin trace log, which is the primary way to see what a specific plugin run actually did, without attaching a live debugger.
Depth and infinite loops
A plugin's own update can re-trigger itself, or trigger another plugin that triggers the first one again. The execution context's Depth property counts how many plugin calls deep the current chain already is, and the platform enforces a maximum (commonly around 8) before throwing an error rather than looping forever.
Relying on the platform's limit alone isn't good practice. Two better habits:
- Register the step with a filtering attribute list so it only fires on the fields it actually cares about
- Compare the new value against a pre-image before writing, and skip the update if nothing actually changed
| Concept | What it's for |
|---|---|
InvalidPluginExecutionException | Blocking an operation with a clear, user-facing message |
ITracingService | Writing diagnostic messages to the plugin trace log |
context.Depth | Detecting/guarding against a plugin re-triggering itself |
InvalidPluginExecutionException so the user gets a clear message instead of a raw error. Guard against re-trigger loops with filtering attributes and pre-image comparisons — don't rely on the platform's depth limit as your only safety net.