{"record":{"id":"3f40c3b99981369b","repo":"antiwork/gumroad","slug":"typia-assert-error-string-json-error","errorCode":null,"errorMessage":"typia.assert<{ error: string }>(json).error","messagePattern":"typia\\.assert<(.+?)>\\(json\\)\\.error","errorType":"exception","errorClass":"ResponseError","httpStatus":null,"severity":"error","filePath":"app/javascript/data/installments.ts","lineNumber":176,"sourceCode":"  });\n\n  if (!response.ok) throw new ResponseError();\n  // `count` is null when the audience is too large to count within the request; the\n  // resend itself still works (recipients are resolved in a background job).\n  return typia.assert<{ count: number | null; recently_resent: boolean; audience_filtered_out: boolean }>(\n    await response.json(),\n  );\n}\n\nexport async function resendToNonOpeners(externalId: string) {\n  const response = await request({\n    method: \"POST\",\n    accept: \"json\",\n    url: Routes.internal_installment_non_opener_resend_path(externalId),\n  });\n\n  const json: unknown = await response.json();\n  if (!response.ok) throw new ResponseError(typia.assert<{ error: string }>(json).error);\n  return typia.assert<{ success: boolean }>(json);\n}\n\nexport async function previewInstallment(externalId: string) {\n  const response = await request({\n    method: \"POST\",\n    accept: \"json\",\n    url: Routes.internal_installment_preview_email_path(externalId),\n  });\n\n  if (!response.ok) throw new ResponseError(typia.assert<{ message: string }>(await response.json()).message);\n}\n","sourceCodeStart":158,"sourceCodeEnd":189,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/javascript/data/installments.ts#L158-L189","documentation":"resendToNonOpeners POSTs to the non_opener_resend path; on non-ok it runs typia.assert<{ error: string }>(json) and throws ResponseError with the server's error text. Two distinct failures live on this line: the expected ResponseError when the server sent { error }, and a typia TypeError thrown from inside the assert when the failure body doesn't match that exact shape — an HTML error page, an empty body, or a body keyed message instead of error.","triggerScenarios":"A 4xx whose body is JSON { error } → ResponseError with that message. A proxy or Rails exception page (HTML), an empty 4xx body, or a body like { message: '...' } → typia TypeError (invalid type at $input.error) instead of the intended ResponseError.","commonSituations":"A rate-limited internal endpoint whose 429 body has no error key; a controller rescue block returning head :no_content on failure; an environment behind an HTML error proxy where the JSON contract breaks only on the error path.","solutions":["Inspect the failing response body in the Network tab — does it actually contain { error: string }?","Make the extraction defensive: parse once, type-guard the error key, and fall back to a generic message instead of typia.assert on an error path.","Align the server: on failure render json: { error: ... } with a matching status.","If the failure is 429, note request() already raises RateLimitError before your handler — don't re-parse that body."],"exampleFix":"// before\nif (!response.ok) throw new ResponseError(typia.assert<{ error: string }>(json).error);\n\n// after\nif (!response.ok) {\n  const message =\n    typeof json === 'object' && json !== null && 'error' in json && typeof (json as { error: unknown }).error === 'string'\n      ? (json as { error: string }).error\n      : 'Could not resend to non-openers.';\n  throw new ResponseError(message);\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"const hasErrorBody = (json: unknown): json is { error: string } =>\n  typeof json === 'object' && json !== null && 'error' in json && typeof (json as { error: unknown }).error === 'string';","tryCatchPattern":"try {\n  const result = await resendToNonOpeners(externalId);\n} catch (e) {\n  if (!(e instanceof ResponseError)) {\n    // typia TypeError: the failure body wasn't { error: string } — log it whole\n    console.error('Unexpected error body', e);\n    showError('Could not resend to non-openers.');\n  } else {\n    showError(e.message);\n  }\n}","preventionTips":["Never run typia.assert on an error-path body; use a type guard with a fallback message.","Pin every internal endpoint to one error-body key (error vs message) and enforce it in tests.","Return RateLimitError semantics for 429 rather than parsing bodies at the call site.","Add a controller test asserting the failure body shape for every json endpoint."],"tags":["installments","internal-tools","typia","schema-validation","error-handling"],"backgroundTag":"api-error-response-body","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}