{"record":{"id":"86540ce7ba645d38","repo":"remix-run/react-router","slug":"no-contact-found-for-id","errorCode":null,"errorMessage":"No contact found for ${id}","messagePattern":"No contact found for (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tutorials/address-book/app/data.ts","lineNumber":88,"sourceCode":"      keys: [\"first\", \"last\"],\n    });\n  }\n  return contacts.sort(sortBy(\"last\", \"createdAt\"));\n}\n\nexport async function createEmptyContact() {\n  const contact = await fakeContacts.create({});\n  return contact;\n}\n\nexport async function getContact(id: string) {\n  return fakeContacts.get(id);\n}\n\nexport async function updateContact(id: string, updates: ContactMutation) {\n  const contact = await fakeContacts.get(id);\n  if (!contact) {\n    throw new Error(`No contact found for ${id}`);\n  }\n  await fakeContacts.set(id, { ...contact, ...updates });\n  return contact;\n}\n\nexport async function deleteContact(id: string) {\n  fakeContacts.destroy(id);\n}\n\n[\n  {\n    avatar:\n      \"https://sessionize.com/image/124e-400o400o2-wHVdAuNaxi8KJrgtN3ZKci.jpg\",\n    first: \"Shruti\",\n    last: \"Kapoor\",\n    twitter: \"@shrutikapoor08\",\n  },\n  {","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/remix-run/react-router/blob/1fd704a7dabcbe3ae09d7387b460e6acaba30ec1/tutorials/address-book/app/data.ts#L70-L106","documentation":"This is a domain error thrown by updateContact() in tutorials/address-book/app/data.ts when fakeContacts.get(id) returns a falsy value (undefined/null). It guards against blindly merging updates onto a missing record in the in-memory fakeContacts store, which would otherwise create a partial/garbage contact. Because getContact() (which does not throw) is the read path, callers learn of a missing record only when they attempt to mutate it.","triggerScenarios":"Calling updateContact(id, updates) with an id that was never created by createEmptyContact(), was removed by deleteContact() (fakeContacts.destroy(id)), or whose value differs from the URL param after navigation (stale link, manual URL edit, concurrent deletion). Also triggered by a loader/action receiving a params.contactId that does not match any key in the fakeContacts store.","commonSituations":"User opens a contact edit route via a bookmarked or refreshed URL for a contact that was deleted in another tab (fakeContacts.destroy ran); tutorial tinkerers call updateContact directly with a hard-coded id; the route param is `contacts/:contactId` but the action passes `params.id`; the seed data was reset (HMR/module reload recreated fakeContacts) so prior ids no longer exist.","solutions":["Before updateContact(), confirm the contact exists by awaiting getContact(id) and returning a 404 / 'Not Found' Response when it is falsy — mirrors the route's existing read-path pattern.","If the record legitimately may be absent, catch the error in the action and return a 404 Response so React Router renders the boundary instead of crashing the route.","Ensure the id passed to updateContact comes from params.contactId (matching the route definition) and not from a stale client cache or form field.","If using the tutorial with HMR, restart the dev server so fakeContacts seed data is consistent with the ids in the URL."],"exampleFix":"// before\nexport async function updateContact(id: string, updates: ContactMutation) {\n  const contact = await fakeContacts.get(id);\n  if (!contact) {\n    throw new Error(`No contact found for ${id}`);\n  }\n  await fakeContacts.set(id, { ...contact, ...updates });\n  return contact;\n}\n\n// after (guard in the action, fail with a Response)\nexport async function updateContact(id: string, updates: ContactMutation) {\n  const contact = await fakeContacts.get(id);\n  if (!contact) {\n    throw new Response(`No contact found for ${id}`, { status: 404 });\n  }\n  await fakeContacts.set(id, { ...contact, ...updates });\n  return contact;\n}","handlingStrategy":"validation","validationCode":"// Validate existence before mutating, in the route action or caller.\nimport { getContact } from \"~/data\";\n\nexport async function action({ params, request }: ActionFunctionArgs) {\n  const id = params.contactId!;\n  const existing = await getContact(id);\n  if (!existing) {\n    throw new Response(\"Contact not found\", { status: 404 });\n  }\n  const formData = await request.formData();\n  await updateContact(id, Object.fromEntries(formData));\n  return redirect(`/contacts/${id}`);\n}","typeGuard":"// Narrow a possibly-missing contact before update.\nimport type { ContactRecord } from \"~/data\";\n\nconst isContact = (value: unknown): value is ContactRecord =>\n  typeof value === \"object\" &&\n  value !== null &&\n  \"id\" in value &&\n  typeof (value as ContactRecord).id === \"string\";","tryCatchPattern":"// In an action: let it surface as a 404 Response instead of an uncaught Error.\ntry {\n  await updateContact(id, updates);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith(\"No contact found for \")) {\n    throw new Response(err.message, { status: 404 });\n  }\n  throw err; // unexpected — propagate\n}","preventionTips":["Treat getContact() returning undefined as the source of truth for 'not found'; reserve updateContact() for records you have already confirmed exist.","Derive the id only from the matched route params (params.contactId) so stale or hand-edited ids are caught by the router first.","After deleteContact(), redirect away from any edit route for that id so a subsequent save cannot target a destroyed record.","Prefer throwing a Response with status 404 from data-layer errors so React Router's error boundary renders the intended UI.","When iterating on the tutorial with HMR, restart the dev server if seed data appears to reset, so in-memory ids stay consistent."],"tags":["data-layer","not-found","tutorial","domain-validation","crud"],"backgroundTag":null,"analyzedSha":"1fd704a7dabcbe3ae09d7387b460e6acaba30ec1","analyzedAt":"2026-08-12T13:54:57.804Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}