{"record":{"id":"6d46206a86475fd4","repo":"antiwork/gumroad","slug":"json-error-message-6d4620","errorCode":null,"errorMessage":"json.error_message","messagePattern":"json\\.error_message","errorType":"exception","errorClass":"ResponseError","httpStatus":null,"severity":"error","filePath":"app/javascript/data/profile_settings.ts","lineNumber":93,"sourceCode":"  const hasSellerProfileChanges = Object.values(sellerProfile).some((value) => value !== undefined);\n  const response = await request({\n    method: \"PUT\",\n    url: Routes.profile_path(),\n    accept: \"json\",\n    data: {\n      user,\n      profile_picture_blob_id,\n      ...(hasSellerProfileChanges ? { seller_profile: sellerProfile } : {}),\n      // Omit pages/sections entirely when the caller didn't pass them, so a settings-only save\n      // doesn't replace (and prune) the server's section list. When they are sent, profile_version\n      // lets the server reject the write if the layout changed elsewhere since this editor loaded.\n      ...(tabs !== undefined ? { tabs } : {}),\n      ...(sections !== undefined ? { sections } : {}),\n      ...(profileVersion !== undefined ? { profile_version: profileVersion } : {}),\n    },\n  });\n  const json = typia.assert<{ success: false; error_message: string } | { success: true }>(await response.json());\n  if (!json.success) throw new ResponseError(json.error_message);\n};\n\nexport const getProduct = async (id: string) => {\n  const response = await request({\n    method: \"GET\",\n    url: Routes.profile_product_path(id),\n    accept: \"json\",\n  });\n  if (!response.ok) throw new ResponseError();\n  return typia.assert<ProductProps>(await response.json());\n};\n\nexport const unlinkTwitter = async () => {\n  const response = await request({\n    method: \"POST\",\n    url: Routes.unlink_twitter_settings_connections_path(),\n    accept: \"json\",\n  });","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/javascript/data/profile_settings.ts#L75-L111","documentation":"ResponseError thrown at profile_settings.ts:93 whose message is json.error_message from the PUT to Routes.profile_path(): updateProfileSettings sent the user attributes, optional seller_profile (font/background/highlight), optional tabs/sections and an optional profile_version, and the server answered { success: false, error_message }. The comments in the payload construction matter: tabs/sections are omitted unless provided so a settings-only save doesn't prune the server's layout, and profile_version lets the server reject the write if the layout changed elsewhere — a deliberate optimistic-concurrency guard whose conflict message arrives through this exact throw.","triggerScenarios":"PUT profile_path answers success:false with error_message: profile_version conflict (the layout changed since this editor loaded — another tab, the mobile app, or a server-side migration edited sections); validation on user attributes (username taken, email invalid, bio too long); a sections/tabs payload containing an unknown section type; or seller_profile-only fields sent under user where the profile policy rejects them.","commonSituations":"Two editor tabs open on the same profile — both save and the second gets the version-conflict message; user edits the profile in the mobile app while the web editor sits open; a section type renamed server-side leaving the editor sending stale shapes; username/email validation failures surfacing through the same generic throw.","solutions":["Read error_message first: a version-conflict message means refetch profile settings and re-apply the edit; other messages name the rejected attribute","Only send tabs/sections when the caller truly has them (the spread already enforces this) — never send empty arrays to 'clear' unless that is intended, since the server prunes to what is sent","Keep font/background_color/highlight_color in the seller_profile payload; placing them under user gets them rejected by the profile policy","On conflict, re-fetch the profile, merge the user's pending changes over the fresh version, and retry once with the new profile_version","Surfacing error_message in the editor UI (rather than a toast with the generic default) is what makes the conflict recoverable by the user"],"exampleFix":"// before — blind retry on conflict loses the user's edit\ntry { await updateProfileSettings(p); } catch { await updateProfileSettings(p); }\n\n// after — refetch version on conflict, re-apply once\ntry {\n  await updateProfileSettings(p);\n} catch (e) {\n  assertResponseError(e);\n  if (!e.message.toLowerCase().includes('changed')) throw e;\n  const fresh = await fetchProfileSettings(); // carries fresh profile_version\n  await updateProfileSettings({ ...p, profileVersion: fresh.profile_version });\n}","handlingStrategy":"retry","validationCode":"// send the version guard only when you have a fresh one\nconst versionedPayload = (p: Partial<ProfileSettings>) =>\n  p.profileVersion == null ? { ...p, profileVersion: freshProfileVersion() } : p;","typeGuard":"import { assertResponseError } from '$app/utils/request';\nassertResponseError(e); // e.message distinguishes version conflict from attribute rejection","tryCatchPattern":"try {\n  await updateProfileSettings(p);\n} catch (e) {\n  assertResponseError(e);\n  if (!isConflictMessage(e.message)) { showError(e.message); return; }\n  const fresh = await fetchProfileSettings();            // get new profile_version\n  await updateProfileSettings({ ...p, profileVersion: fresh.profile_version }); // one merge-retry\n}","preventionTips":["Always send profile_version when sending tabs/sections — it's the guard against silent clobbering","On a conflict message, refetch and re-apply once instead of blind-retrying the same PUT","Omit tabs/sections entirely on settings-only saves (the spread in the source already does this)","Show error_message in the editor UI so users can act on the server's reason"],"tags":["profile-settings","optimistic-concurrency","server-validation","dashboard"],"backgroundTag":"json-api-error-envelope","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}