laurent22/joplin · error · ErrorConflict
This item is already present and cannot be added again
Error message
This item is already present and cannot be added again: ${item.name} What it means
When inserting an item hits a unique-constraint conflict on the server, ItemModel tries to locate the existing item by owner and name. If that lookup finds nothing (e.g. jop_id/id mismatch or an inconsistent constraint), it cannot treat the insert as an update, so it throws ErrorConflict to the client.
Solutions
- Retry the upload after a short backoff — the concurrent request may finish and the item become visible.
- Verify the item id/jop_id/owner_id sent matches the existing item; mismatches can defeat the lookup.
- Re-sync the client so server state is authoritative before re-uploading.
- If it persists, inspect server logs — the modelLogger.error line records the failing identifiers.
Example fix
// before
await api.post('/items', item); // throws ErrorConflict
// after
try {
await api.post('/items', item);
} catch (e) {
if (e.message.includes('already present')) await sleep(backoff).then(() => api.put(`/items/${existingId}`, item));
else throw e;
} Defensive patterns
Strategy: retry
Validate before calling
const existing = await this.loadByName(item.owner_id || userId, item.name, { fields: ['id'] });
if (existing) return this.save({ ...item, id: existing.id }, { isNew: false }); Type guard
null
Try / catch
try { await itemModel.save(item, { isNew: true }); } catch (e) { if (e instanceof ErrorConflict) await sleep(500).then(() => itemModel.save(item, { isNew: false })); else throw e; } Prevention
- Upload idempotently using stable item ids/names
- Serialize uploads of the same item across clients
- Check for the item before creating (loadByName first)
- Monitor server logs for unique-constraint errors to find racy sync paths
When it happens
Trigger: A PUT/POST upload that races with a concurrent request creating the same item name, where the subsequent loadByName(owner_id, name) returns null despite the duplicate-key error.
Common situations: Two clients uploading the same item simultaneously; retries after a network timeout where the server-side state is inconsistent; sync engines colliding on item names for the same user.
Understand the failure class
Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.
Related errors
- Cannot change the note lock of a conflict note
- processingPathTwice
- rejectedByTarget
- Remote item has an updated_time in the future
- 404
AI-assisted analysis of laurent22/joplin@981a03c5c9 (2026-09-17).
Data as JSON: /api/errors/eb0eaf2a95eef7c5.
Report an issue: GitHub.
Appendix: source
Thrown at packages/server/src/models/ItemModel.ts:1207
// Savepoint needed because on Postgres a failed statement aborts the whole
// transaction, and we recover from the unique constraint error below.
const savePoint = await this.setSavePoint();
try {
item = await super.save(item, options);
await this.releaseSavePoint(savePoint);
} catch (error) {
await this.rollbackSavePoint(savePoint);
if (isUniqueConstraintError(error)) {
// The item was created by a concurrent request - typically a client
// retrying an upload that is still being processed. Save it as an update
// so that the retry is not treated as an error.
const existingItem = await this.loadByName(item.owner_id || userId, item.name, { fields: ['id'] });
if (!existingItem) {
modelLogger.error(`Unique constraint error on item, but the item could not be found: ${JSON.stringify({ id: item.id, name: item.name, jop_id: item.jop_id, owner_id: item.owner_id })}`, error);
throw new ErrorConflict(`This item is already present and cannot be added again: ${item.name}`);
}
modelLogger.info(`Item was created by a concurrent request - updating it instead: ${JSON.stringify({ name: item.name, jop_id: item.jop_id, owner_id: item.owner_id })}`);
isNew = false;
item = await super.save({ ...item, id: existingItem.id }, { ...options, isNew: false });
} else {
throw error;
}
}
if (isNew) await this.models().userItem().add(userId, item.id);
// We only record updates. Create and Delete events are recorded elsewhere.
const changeItemName = item.name || previousName;
if (!isNew && this.shouldRecordChange(changeItemName)) {
await this.models().change().recordChange({View on GitHub (pinned to 981a03c5c9)