If you’ve been working with Adobe Experience Manager (AEM), chances are you’ve run into one of these pesky exceptions:
- javax.jcr.InvalidItemStateException: Unable to update a stale item
- org.apache.jackrabbit.core.state.StaleItemStateException: [UUID] has been modified externally
- OakState0001: Unresolved conflicts in…
These errors usually occur during a save operation (resourceResolver.commit() or session.save()) when AEM detects that something has changed in the background, and your current session is now outdated or conflicted.
Let’s break it down in plain language: AEM is telling you, “Hey, this item was changed by something else, and I can’t trust your current state to save anymore.”
The Root Cause: Concurrency and Out-of-Sync Data
In AEM, content is stored in the JCR (Java Content Repository). When you get a JCR session, you’re essentially getting a “snapshot” of the repository at that moment. If another process modifies the same piece of content after you got your snapshot but before you try to save your changes, your session’s view becomes “stale.” When you try to save your modifications to a stale item, AEM, in its wisdom, throws these exceptions to prevent data corruption.
Common Scenarios Leading to These Errors
These errors often surface in scenarios where multiple operations are happening on the same content simultaneously. Let’s look at some classic examples:
1. Multiple Workflows Touching the Same Resource
Imagine an asset being uploaded. “Dam Update Asset” starts processing it (e.g., generating renditions). Before it finishes and saves, “Approval” workflow also kicks off on the same asset. If both try to write to the asset simultaneously or in quick succession without proper handling, you’re in for a stale item error.
2. Parallel Scheduler Jobs
If you have a custom scheduler that processes content, and due to configuration or unexpected load, it starts running multiple instances of the same job in parallel on the same content, conflicts are almost guaranteed.
3. Two Parallel Jobs Running:
Similar to schedulers, if you have any custom code that spawns multiple threads or jobs that might operate on the same JCR path, you need to be mindful of concurrent access.
4. Long-Running Sessions
If your code opens a session, does a lot of processing, and then tries to save changes after a long time, the chances of the underlying content being modified by another process during that interval increase significantly.
How to Resolve and Prevent These Errors
Here are some simple and effective ways to avoid and fix these issues:
1. Keep Sessions Short
Don’t hold onto a JCR session for too long. Use it, do the job, and close it. The longer a session is open, the higher the chance of its view becoming stale. This is a best practice for performance and stability too.
Treat your JCR session like milk — it can go stale if you leave it out too long. 🥛🙂
2. Refresh Session Before Save
If some piece of code is prone to session conflicts, refresh your session before committing changes.
resourceResolver.refresh();
resourceResolver.commit();
Before making any changes, call resourceResolver.refresh(). This refreshes your session to the current repository state (HEAD), helping to reduce the likelihood of conflicts and exceptions during resourceResolver.commit(). The resolver is updated to reflect the latest state. Resources which have changes pending are not discarded.
You can also opt to utilize RepositoryException for refreshing session on need basis, please find the details on https://cqdump.joerghoh.de/2015/11/02/aem-anti-pattern-long-running-sessions/
3. Avoid Overusing .adaptTo()
While adaptTo is powerful, using it excessively, especially to adapt a Resource to an Asset + ModifiableValueMap + some more API, can sometimes lead to subtle issues if not managed carefully.
Always consider if you truly need the adapted object or if ResourceResolver and ValueMap operations suffice. If you’re only updating metadata, use ModifiableValueMap.
Keep things clean and minimal.
4. Prefer Resource APIs Over Node APIs
Node-based operations (javax.jcr.Node) are more prone to session-related errors and harder to manage. Use Sling’s ResourceResolver and Resource APIs when possible.
Resource APIs are generally more aligned with AEM’s modern development paradigm and are often more convenient for content manipulation. They abstract away some of the lower-level JCR complexities.
5. Check What the Exception Message Is Telling You
Always read the full stack trace and message carefully. Is the error on the asset itself? Or maybe it’s on a parent folder or renditions?
You might be trying to save an asset, but the real conflict is on a higher-level folder updated by another process.
6. Sling Job Scheduler for Asynchronous Tasks:
For any long-running or potentially conflicting operations, leverage AEM’s Sling Job Scheduler. It provides a robust framework for asynchronous task execution, including retry mechanisms and preventing simultaneous execution of the same job. This is far better than custom schedulers that might spawn parallel instances.
Best Practices Checklist
| Practice | Description |
| ✅ Keep sessions short | Open, do work, save, close |
| ✅ Refresh before save | resourceResolver.refresh() |
| ✅ Prefer Resource APIs | Avoid JCR Node unless necessary |
| ✅ Avoid too many adaptTo() | Especially in loops or deep trees |
| ✅ Handle concurrency safely | Use coordination between workflows/schedulers |
| ✅ Read exception message carefully | Look at the actual path it’s failing on |