laurent22/joplin · error

Cannot change the note lock of a conflict note

Error message

Cannot change the note lock of a conflict note

What it means

A generic validation guard in checkCanChangeLockState that rejects any attempt to enable or disable a lock on a note whose type is NOTE_TYPE_CONFLICT. Conflict notes are temporary merge/sync artifacts that Joplin manages internally, so a user- or plugin-driven lock change on them is never valid; the input at fault is a conflict-type NoteEntity passed to enableNoteLock or disableNoteLock (e.g. via commands invoked from keyboard shortcuts or plugins, since the UI normally hides these actions for conflict notes).

Solutions

  1. Resolve the conflict (merge/choose a version) so the note is no longer a conflict copy, then lock the resolved note.
  2. Skip notes with `is_conflict` set when batch-locking.
  3. Catch the error and ignore conflict notes.
  4. Lock the original note rather than its conflict copy.

Example fix

// before
await joplin.commands.execute('enableNoteLock', conflictNoteId);
// after
const note = await Note.load(conflictNoteId);
if (note && !note.is_conflict) await joplin.commands.execute('enableNoteLock', conflictNoteId);
Defensive patterns

Strategy: validation

Validate before calling

const note = await Note.load(noteId);
if (note && !note.is_conflict) await joplin.commands.execute('enableNoteLock', noteId);

Type guard

const isResolvedNote = (n: NoteEntity): boolean => !n.is_conflict;

Try / catch

try { await enableNoteLock(id); } catch (e) { if (e.message.includes('conflict note')) notify('Resolve conflict first'); else throw e; }

Prevention

When it happens

Trigger: Invoking enableNoteLock/disableNoteLock on a note with `is_conflict === true`, which appears in the conflict folder and is produced by sync conflict resolution.

Common situations: Plugins processing all notes in the conflict folder; automation that locks recently modified notes after a sync generated conflicts.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of laurent22/joplin@981a03c5c9 (2026-09-17). Data as JSON: /api/errors/bd36f969559fb489. Report an issue: GitHub.

Appendix: source

Thrown at packages/lib/services/noteLock/setNoteLockState.ts:18

import { ModelType } from '../../BaseModel';
import BaseItem from '../../models/BaseItem';
import ItemChange from '../../models/ItemChange';
import Note from '../../models/Note';
import Setting from '../../models/Setting';
import { itemIsReadOnlySync, ItemSlice } from '../../models/utils/readOnly';
import { NoteEntity } from '../database/types';
import eventManager, { EventName } from '../../eventManager';
import isNoteLockEnabled from './isNoteLockEnabled';
import NoteLockSession from './NoteLockSession';

// The UI hides the enable/disable actions for these cases, but the commands can also be
// invoked directly (keyboard, plugins), so the transitions fail closed here too.
const checkCanChangeLockState = (note: NoteEntity, noteId: string) => {
	if (!isNoteLockEnabled()) throw new Error('Note lock is not enabled');
	if (!note) throw new Error(`No such note: ${noteId}`);
	if (note.deleted_time) throw new Error('Cannot change the note lock of a deleted note');
	if (note.is_conflict) throw new Error('Cannot change the note lock of a conflict note');
	if (itemIsReadOnlySync(ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, note as ItemSlice, Setting.value('sync.userId'), BaseItem.syncShareCache)) throw new Error('Cannot change the note lock of a read-only note');
	if (!NoteLockSession.instance().isUnlocked()) throw new Error('Cannot change the note lock while the session is locked');
};

const validationFields = ['id', 'is_locked', 'deleted_time', 'is_conflict', 'share_id'];

// These only validate and emit: the note screen listens for the event and persists the
// change with a scheduled gated save.
export const enableNoteLock = async (noteId: string) => {
	const note = await Note.load(noteId, { fields: validationFields });
	checkCanChangeLockState(note, noteId);
	if (note.is_locked) throw new Error(`Note is already locked: ${noteId}`);
	eventManager.emit(EventName.NoteLockNoteStateChange, { noteId, isLocked: true });
};

export const disableNoteLock = async (noteId: string) => {
	const note = await Note.load(noteId, { fields: validationFields });
	checkCanChangeLockState(note, noteId);

View on GitHub (pinned to 981a03c5c9)