{"record":{"id":"bd236c8b391f205c","repo":"RocketChat/Rocket.Chat","slug":"error-message-change-to-thread","errorCode":"error-message-change-to-thread","errorMessage":"Cannot update message to a thread","messagePattern":"Cannot update message to a thread","errorType":"exception","errorClass":"Meteor.Error","httpStatus":null,"severity":"error","filePath":"apps/meteor/server/meteor-methods/messages/updateMessage.ts","lineNumber":49,"sourceCode":"\t\t}\n\t});\n\n\t// IF the message has custom fields, always update\n\t// Ideally, we'll compare the custom fields to check for change, but since we don't know the shape of\n\t// custom fields, as it's user defined, we're gonna update\n\tconst msgText = originalMessage?.attachments?.[0]?.description ?? originalMessage.msg;\n\tif (msgText === message.msg && !previewUrls && !message.customFields) {\n\t\treturn;\n\t}\n\n\tif (!!message.tmid && originalMessage._id === message.tmid) {\n\t\tthrow new Meteor.Error('error-message-same-as-tmid', 'Cannot set tmid the same as the _id', {\n\t\t\tmethod: 'updateMessage',\n\t\t});\n\t}\n\n\tif (!originalMessage.tmid && !!message.tmid) {\n\t\tthrow new Meteor.Error('error-message-change-to-thread', 'Cannot update message to a thread', { method: 'updateMessage' });\n\t}\n\n\tconst _hasPermission = await hasPermissionAsync(uid, 'edit-message', message.rid);\n\tconst editAllowed = settings.get('Message_AllowEditing');\n\tconst editOwn = originalMessage.u && originalMessage.u._id === uid;\n\n\tif (!_hasPermission && (!editAllowed || !editOwn)) {\n\t\tthrow new Meteor.Error('error-action-not-allowed', 'Message editing not allowed', {\n\t\t\tmethod: 'updateMessage',\n\t\t\taction: 'Message_editing',\n\t\t});\n\t}\n\n\tconst blockEditInMinutes = settings.get('Message_AllowEditing_BlockEditInMinutes');\n\tconst bypassBlockTimeLimit = await hasPermissionAsync(uid, 'bypass-time-limit-edit-and-delete', message.rid);\n\n\tif (!bypassBlockTimeLimit && Match.test(blockEditInMinutes, Number) && blockEditInMinutes !== 0) {\n\t\tlet currentTsDiff = 0;","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/server/meteor-methods/messages/updateMessage.ts#L31-L67","documentation":"Thrown by Rocket.Chat's updateMessage when the stored message has no tmid (a top-level channel message) but the edit payload includes one. Messages cannot be moved into a thread by editing; thread membership is fixed at creation time. The server enforces this by comparing originalMessage.tmid against the incoming message.tmid.","triggerScenarios":"Calling Meteor.call('updateMessage', { ...msg, tmid }) where the stored message has no tmid — e.g. an edit form pre-filled with thread context, or payload builders that unconditionally attach tmid to every edit call.","commonSituations":"An 'edit in thread' UI applied to a message originally posted directly to the channel; bots reusing thread-reply payload shapes for all edits; attempts to retro-organize old messages into threads via bulk edit.","solutions":["Strip tmid from the edit payload; send only the fields being changed","To get the content into a thread: send a new message with tmid of the desired parent, then delete the original","Guard payload construction: include tmid only when editing a message that already has one"],"exampleFix":"// before\nMeteor.call('updateMessage', { _id: msg._id, msg: text, tmid: parent._id }); // msg is not threaded\n\n// after\nMeteor.call('updateMessage', { _id: msg._id, msg: text });\n// to thread it instead: post a new reply\n// Meteor.call('sendMessage', { rid, msg: text, tmid: parent._id })","handlingStrategy":"validation","validationCode":"const mayIncludeTmid = (original: { tmid?: string }, payload: { tmid?: string }): boolean =>\n  !payload.tmid || !!original.tmid;\nif (!mayIncludeTmid(originalMessage, payload)) {\n  delete payload.tmid; // a non-thread message cannot be edited into a thread\n}","typeGuard":"const isChangeToThreadError = (e: unknown): e is Meteor.Error =>\n  typeof e === 'object' && e !== null && (e as { error?: string }).error === 'error-message-change-to-thread';","tryCatchPattern":"try {\n  await Meteor.callAsync('updateMessage', payload);\n} catch (e) {\n  if (isChangeToThreadError(e)) {\n    const { tmid, ...rest } = payload;\n    return Meteor.callAsync('updateMessage', rest);\n  }\n  throw e;\n}","preventionTips":["Type edit payloads as Omit<Message, 'tmid'> so the field is excluded at compile time","Never include tmid in edit payloads; thread membership is immutable","When content belongs in a thread, post a new threaded message instead of editing"],"tags":["rocket-chat","meteor","message-editing","threads","validation"],"backgroundTag":"immutable-field-update","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}