{"record":{"id":"7c5390c80185893f","repo":"sickn33/agentic-awesome-skills","slug":"api-error-result-left-type","errorCode":null,"errorMessage":"API error: ${result.left.type}","messagePattern":"API error: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"skills/fp-refactor/SKILL.md","lineNumber":1492,"sourceCode":"\n```typescript\n// Wrap external API calls first\nconst fetchUserApi = (id: string): TE.TaskEither<ApiError, UserDto> =>\n  pipe(\n    TE.tryCatch(\n      () => externalApiClient.getUser(id),\n      (e) => ({ type: 'api_error' as const, cause: e })\n    )\n  );\n\n// Internal code can stay imperative initially\nasync function handleUserRequest(userId: string) {\n  const result = await fetchUserApi(userId)();\n  if (E.isRight(result)) {\n    // Process user with existing code\n    return processUser(result.right);\n  } else {\n    throw new Error(`API error: ${result.left.type}`);\n  }\n}\n```\n\n### Strategy 2: Create Bridge Functions\n\nBuild helpers to convert between fp-ts and imperative code:\n\n```typescript\n// Bridge from Either to thrown errors\nconst unsafeUnwrap = <E, A>(either: E.Either<E, A>): A =>\n  pipe(\n    either,\n    E.getOrElseW((e) => {\n      throw e instanceof Error ? e : new Error(String(e));\n    })\n  );\n","sourceCodeStart":1474,"sourceCodeEnd":1510,"githubUrl":"https://github.com/sickn33/agentic-awesome-skills/blob/58d857988fcfac6986206bca2b2fe223aa437e4b/skills/fp-refactor/SKILL.md#L1474-L1510","documentation":"The bridge function in fp-refactor re-throws when the fp-ts TaskEither result is Left, stringifying result.left.type into the message. It exists to show interop between Either-returning code and a legacy throw-based caller during incremental migration.","triggerScenarios":"handleUserRequest called when fetchUserApi fails — network error, non-2xx API status, or decode failure — so E.isRight(result) is false and the else branch throws.","commonSituations":"Gradual fp-ts adoption where old layers still expect exceptions; error.type being undefined because the Left holds a plain Error rather than a tagged object, yielding 'API error: undefined'.","solutions":["Ensure the Left channel always carries a tagged error object with a `type` field so the message is meaningful","Catch this throw at the API boundary and map to an HTTP status instead of letting it 500","Once callers migrate, remove the throw and branch on the Either directly"],"exampleFix":"// before\n} else {\n  throw new Error(`API error: ${result.left.type}`)\n}\n\n// after: safe rendering of any Left shape\n} else {\n  const left = result.left as { type?: string; message?: string }\n  throw new Error(`API error: ${left.type ?? left.message ?? 'unknown'}`)\n}","handlingStrategy":"type-guard","validationCode":"// ensure Left always carries { type: string } before bridging\nconst tagged = E.mapLeft((e: unknown) =>\n  e && typeof e === 'object' && 'type' in e ? e : { type: 'unknown' }\n)(result)","typeGuard":"const isTaggedError = (x: unknown): x is { type: string } =>\n  typeof x === 'object' && x !== null && typeof (x as any).type === 'string'","tryCatchPattern":"try {\n  await handleUserRequest(userId)\n} catch (e) {\n  // map API error bridge throws to 502/504 with the type preserved\n  res.status(502).json({ error: String(e) })\n}","preventionTips":["Standardize Left payloads as tagged objects with a type field","Migrate callers to branch on Either directly and delete throw bridges"],"tags":["fp-ts","interop","error-bridge","either"],"backgroundTag":"either-left-unhandled","analyzedSha":"58d857988fcfac6986206bca2b2fe223aa437e4b","analyzedAt":"2026-08-26T11:55:59.350Z","schemaVersion":2},"datasetVersion":"2026-08-26T14:46:13.012Z"}