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

  1. Retry the upload after a short backoff — the concurrent request may finish and the item become visible.
  2. Verify the item id/jop_id/owner_id sent matches the existing item; mismatches can defeat the lookup.
  3. Re-sync the client so server state is authoritative before re-uploading.
  4. 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

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


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)