{"record":{"id":"287eb6fcd8e12fe1","repo":"paperclipai/paperclip","slug":"attachment-was-removed","errorCode":null,"errorMessage":"Attachment was removed","messagePattern":"Attachment was removed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"ui/src/components/task-chat/TaskChatComposer.tsx","lineNumber":685,"sourceCode":"    const id = crypto.randomUUID();\n    setAttachments((prev) => [\n      ...prev,\n      {\n        id,\n        name: file.name,\n        size: file.size,\n        inline: true,\n        status: \"uploading\",\n      },\n    ]);\n    try {\n      const attachment = onAttachImage ? await onAttachImage(file) : undefined;\n      const url = onAttachImage\n        ? attachment?.contentPath\n        : await onImageUpload?.(file);\n      if (!url) throw new Error(\"Upload did not return a file URL\");\n      if (!attachmentsRef.current.some((item) => item.id === id))\n        throw new Error(\"Attachment was removed\");\n      setAttachments((prev) =>\n        prev.map((item) =>\n          item.id === id\n            ? {\n                ...item,\n                attachmentId: attachment?.id,\n                contentPath: url,\n                status: \"attached\",\n              }\n            : item,\n        ),\n      );\n      return url;\n    } catch (err) {\n      setAttachments((prev) =>\n        prev.map((item) =>\n          item.id === id\n            ? {","sourceCodeStart":667,"sourceCodeEnd":703,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/ui/src/components/task-chat/TaskChatComposer.tsx#L667-L703","documentation":"In TaskChatComposer's `uploadInlineImage`, after the async upload resolves it re-checks that the placeholder attachment (tracked by `id`) still exists in `attachmentsRef.current`. If the user removed the attachment while the upload was in flight, the composer throws instead of mutating state for a deleted item.","triggerScenarios":"Start an inline image upload, then click remove on that attachment before the upload completes; the resolved upload then finds the id gone from `attachmentsRef.current`.","commonSituations":"Slow uploads on poor networks combined with quick user edits; user hits a 'remove all attachments' action mid-upload; rapid delete/undo interactions.","solutions":["Expected and harmless: catch it in the caller and ignore (attachment intentionally removed) — ensure the catch is silent for this case.","UX fix: disable the per-attachment remove button while `item.uploading` is true.","Alternative: re-add nothing but return early instead of throwing when the id is absent."],"exampleFix":"// before\nif (!attachmentsRef.current.some((item) => item.id === id))\n  throw new Error(\"Attachment was removed\");\n// after\nif (!attachmentsRef.current.some((item) => item.id === id))\n  return; // user removed it mid-upload; nothing to update","handlingStrategy":"try-catch","validationCode":"if (!attachmentsRef.current.some((i) => i.id === id)) return; // still attached?","typeGuard":"const stillAttached = (id: string) => attachmentsRef.current.some((item) => item.id === id);","tryCatchPattern":"try { await uploadInlineImage(file); } catch (e) { if (!/Attachment was removed/.test(e.message)) showToast('Upload failed'); }","preventionTips":["Treat mid-upload removal as an expected no-op, not an error.","Disable remove buttons while an attachment is uploading.","Keep upload results keyed by id and ignore results for deleted ids."],"tags":["race-condition","upload","react"],"backgroundTag":"internal-invariant-violation","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}