Event receivers are the backbone of business logic in classic, on-premises SharePoint. They let you react to list and site changes — adding an item, updating a document, deleting a site — and run your own code in response. They are also one of the easiest places to introduce subtle, hard-to-debug bugs. This article covers how the event model actually works and the patterns that keep receivers reliable in production.

Synchronous vs. asynchronous events

Every SharePoint event comes in one of two flavours, and the distinction matters enormously:

A receiver is just a class deriving from SPItemEventReceiver with the relevant methods overridden:

public class ContractEventReceiver : SPItemEventReceiver
{
    // Synchronous: validate before the item is saved
    public override void ItemAdding(SPItemEventProperties properties)
    {
        base.ItemAdding(properties);

        var amount = properties.AfterProperties["ContractValue"];
        if (amount != null && Convert.ToDecimal(amount) < 0)
        {
            properties.Cancel = true;
            properties.ErrorMessage = "Contract value cannot be negative.";
        }
    }

    // Asynchronous: react after the item is saved
    public override void ItemAdded(SPItemEventProperties properties)
    {
        base.ItemAdded(properties);
        // post-commit work here
    }
}
In synchronous events read user input from properties.AfterProperties (the incoming values) and the existing values from properties.ListItem. In asynchronous events the item is already saved, so properties.ListItem holds the final state.

The recursion trap

The single most common bug in event receivers: an ItemUpdated handler that updates the same item, which fires ItemUpdated again, which updates the item again… The result is an infinite loop, a spike in CPU, and sometimes a stack overflow in the worker process.

The fix is to suppress event firing while you make your own changes by toggling the inherited EventFiringEnabled property:

public override void ItemUpdated(SPItemEventProperties properties)
{
    base.ItemUpdated(properties);

    try
    {
        // Stop our own update from re-triggering this handler
        this.EventFiringEnabled = false;

        SPListItem item = properties.ListItem;
        item["LastReviewed"] = DateTime.UtcNow;
        item.SystemUpdate(false);   // false = don't increment the version
    }
    finally
    {
        // ALWAYS restore in a finally block
        this.EventFiringEnabled = true;
    }
}
Always restore EventFiringEnabled in a finally block. If an exception is thrown while events are disabled and you don't restore the flag, you can silently suppress events for the remainder of that worker-process thread.

SystemUpdate vs. Update

When writing back to the item, pick the right method:

Registering the receiver

For a feature-deployed list, receivers are declared in elements.xml. The Synchronization attribute lets you force an "-ed" event to run synchronously when ordering matters:

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Receivers ListTemplateId="100">
    <Receiver>
      <Name>ContractItemAdded</Name>
      <Type>ItemAdded</Type>
      <Assembly>Contoso.Contracts, Version=1.0.0.0, Culture=neutral,
                PublicKeyToken=abc123def456</Assembly>
      <Class>Contoso.Contracts.ContractEventReceiver</Class>
      <SequenceNumber>10000</SequenceNumber>
      <Synchronization>Synchronous</Synchronization>
    </Receiver>
  </Receivers>
</Elements>

You can also bind receivers programmatically — handy when attaching to an existing list at runtime:

SPList list = web.Lists["Contracts"];
list.EventReceivers.Add(
    SPEventReceiverType.ItemAdded,
    "Contoso.Contracts, Version=1.0.0.0, Culture=neutral, PublicKeyToken=abc123def456",
    "Contoso.Contracts.ContractEventReceiver");

Error handling and performance

Receivers run inside the SharePoint request pipeline, so misbehaving code degrades the whole site. A few hard-won rules:

public override void ItemAdded(SPItemEventProperties properties)
{
    base.ItemAdded(properties);
    try
    {
        // post-commit logic
    }
    catch (Exception ex)
    {
        SPDiagnosticsService.Local.WriteTrace(0,
            new SPDiagnosticsCategory("Contoso.Contracts",
                TraceSeverity.Unexpected, EventSeverity.Error),
            TraceSeverity.Unexpected,
            "ItemAdded failed: {0}", new object[] { ex });
    }
}
Avoid opening elevated privilege blocks unless you truly need them. SPSecurity.RunWithElevatedPrivileges creates a brand-new SPSite in app-pool context — you must open and dispose your own objects inside that delegate, never reuse the ones from properties.

Where this fits today

Server-side event receivers are a full-trust, on-premises (and SharePoint 2013/2016/2019) concept. In SharePoint Online you cannot deploy server-side code at all — the equivalent capabilities moved first to remote event receivers in the add-in model, then to webhooks and, increasingly, Power Automate and Microsoft Graph change notifications. The mental model, though, carries forward: decide whether you need to validate before a change or react after it, and design accordingly.

If you are maintaining a classic farm, the patterns above — synchronous validation, the EventFiringEnabled guard, SystemUpdate, and disciplined logging — will save you most of the production incidents I have seen over the years.

DS

Dhaval Shah

Cloud Solution Architect — 18+ years across SharePoint, Microsoft 365, and Azure AI.