Skip to content

feat(fp): add Try module (try_, tryPromise, attempt, withReporting, classifyError) - #439

Merged
codewizdave merged 4 commits into
stagingfrom
feat/try-module
Aug 20, 2026
Merged

codewizdave merged 4 commits into
stagingfrom
feat/try-module

Conversation

@martyy-code

Copy link
Copy Markdown
Contributor

Delivers the Try<T, E> module that the README and docs/internal/product/features/try.md have been advertising since v1.0. Wraps synchronous and asynchronous throwing functions into a typed value, eliminating silent try/catch blocks at call sites.

What's shipped

  • try_<T>(thunk) and try_<T, E>({ onSuccess, onError }) — sync wrap, with or without an explicit error mapper.
  • tryPromise<T>(thunk) and tryPromise<T, E>({ onSuccess, onError }) — async wrap. onError may itself be async.
  • attempt(config) — returns { execute(), clientSafe() } with optional single-attempt retry and error normalisation.
  • withReporting(onSuccess, name, reporter, metadata?) — forwards caught errors to a caller-supplied ErrorReporter and returns a Result<T, ReportableError>.
  • classifyError(e, rules) — returns 'retryable' | 'non-retryable' based on instanceof matching against a rule list.
  • toResultTry() — converts a Try<T, E> into a Result<T, E> so existing pipe(... , map, getOrElse) pipelines compose naturally.

Plus the standard pipeable surface: mapTry, flatMapTry, mapErrorTry, tapTry, tapAsyncTry, flatMapAsyncTry, matchTry, foldTry, getOrElseTry, getOrThrowTry, getOrNullTry, getOrUndefinedTry, toResultTry, isSuccess, isFailure.

Design choices

  • Internal classes SuccessImpl / FailureImpl follow rule 0014 and live in src/try/internal/; public types are type aliases pointing at them (rule 0012). The factory functions success() / failure() are the only public construction entry points.
  • Discriminated union uses _tag: 'Success' | 'Failure' to mirror the existing Ok/Err and Some/None naming.
  • The { onSuccess, onError } object form is the canonical shape for the wrapping helpers. try_ and tryPromise also keep the thunk-only overload that captures the cause in an UnhandledException.
  • pipe composition across modules works: pipe(tryResult, toResultTry(), map(x => x * 2)) composes a Try pipeable with a Result pipeable.

Verification

  • Type-check: clean
  • Lint: clean
  • Tests: 373/373 pass (27 test files)
  • Coverage: 100% on lines / branches / functions / statements across the 8 covered files
  • Build: clean

Out of scope (deliberately)

The DelayStrategy and RetryConfig types ship for forward compatibility, but no retry / exponential / constant / linear helpers are implemented. attempt().execute() performs at most one retry when retry.shouldRetry(cause) returns true. A retry loop and the delay helpers are planned for a follow-up PR.

The existing docs/internal/product/features/try.md is rewritten to match the shipped surface; references to unimplemented helpers (partition, sanitizeMessage, toClientSafe) are removed.

Closes the gap between the README/ADR 0001 §5 and the source code.

🤖 Generated with Claude Code

…lassifyError)

Delivers the Try<T, E> module that the README and
docs/internal/product/features/try.md have been advertising since
v1.0. Wraps synchronous and asynchronous throwing functions into a
typed value, eliminating silent try/catch blocks at call sites.

- try_<T>(thunk) and try_<T, E>({ onSuccess, onError }) — sync wrap.
- tryPromise<T>(thunk) and tryPromise<T, E>({ onSuccess, onError }) —
  async wrap; onError may itself be async.
- attempt(config) — returns { execute(), clientSafe() } with optional
  single-attempt retry and error normalisation.
- withReporting(onSuccess, name, reporter, metadata?) — forwards
  caught errors to a caller-supplied ErrorReporter.
- classifyError(e, rules) — returns 'retryable' | 'non-retryable'
  based on instanceof matching.
- toResultTry() — converts a Try<T, E> into a Result<T, E> so existing
  pipe(...) pipelines compose naturally.

Internal classes SuccessImpl / FailureImpl follow rule 0014 and live
in src/try/internal/. Public types are type aliases pointing at them
(rule 0012). The discriminated union uses _tag: 'Success' | 'Failure'
to mirror the existing Ok/Err and Some/None naming.

Coverage 100% on lines / branches / functions / statements across the
8 covered files (types.ts and index.ts are excluded by vitest config).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Coverage report

File % Stmts % Branch % Funcs % Lines
Total 100.00% 100.00% 100.00% 100.00%
packages/fp/src/function/compose.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/const-thunks.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/constant.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/endomorphism.ts 0.00% n/a 0.00% 0.00%
packages/fp/src/function/flip.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/flow.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/function-n.ts 0.00% n/a 0.00% 0.00%
packages/fp/src/function/identity.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/index.ts 0.00% n/a 0.00% 0.00%
packages/fp/src/function/lazy.ts 0.00% n/a 0.00% 0.00%
packages/fp/src/function/pipe.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/predicate.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/function/tuple.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/tupled.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/untupled.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/maybe/constants.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/maybe/functions.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/maybe/index.ts 0.00% n/a 0.00% 0.00%
packages/fp/src/maybe/internal/none-impl.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/maybe/internal/some-impl.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/result/attempt.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/result/classify.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/result/constants.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/result/functions.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/result/index.ts 0.00% n/a 0.00% 0.00%
packages/fp/src/result/internal/attempt-impl.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/result/internal/err-impl.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/result/internal/ok-impl.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/result/reporting.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/result/wrapping.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/unit/constants.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/unit/index.ts 0.00% n/a 0.00% 0.00%

Per-file thresholds: 100% on statements / branches / functions / lines (ADR 0002). Files with no branches render n/a in the Branch column. The threshold gate is disabled in this PR and lands with the full method × variant test matrix in a follow-up.

martyy-code and others added 3 commits August 20, 2026 10:39
`attempt()` is now a thin factory returning `new AttemptImpl(config)`.
The class lives in `src/try/internal/attempt-impl.ts` and is not
exported (rule 0014). Construction stays lazy: `attempt()` does not
invoke `onSuccess`; the wrapped operation runs only when `execute()`
or `clientSafe()` is called.

The previous closure-based implementation is preserved verbatim
inside the class. The public surface (`Attempt<T>`, `execute`,
`clientSafe`) is unchanged.

Coverage 100% on lines / branches / functions / statements. Tests for
the impl surface now live in `tests/try/attempt-impl.test.ts`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Try module shipped in #439 duplicated Result one-for-one:
SuccessImpl mirrored OkImpl, FailureImpl mirrored ErrImpl, and
fifteen `*Try` pipeables (`mapTry`, `flatMapTry`, `matchTry`, ...)
shadowed the Result combinators under different names. The
`toResultTry` bridge existed only to cross between the two
isomorphic types.

This commit collapses the reasoning on `Result<T, E>`. There is now
one machine of states, one set of pipeables, one vocabulary.

Public surface changes:

- NEW `Result.fromThrowable(thunk | { onSuccess, onError })` —
  sync wrap. Returns `Result<T, E>`.
- NEW `Result.fromAsyncThrowable(thunk | { onSuccess, onError })` —
  async wrap. Returns `Promise<Result<T, E>>`.
- NEW `UnhandledException`, `AttemptConfig`, `Attempt`,
  `NormalizedError`, `RetryConfig`, `DelayStrategy`,
  `ErrorReporter`, `ErrorContext`, `ReportableError`,
  `ErrorClassification`, `ClassificationRule`, `ErrorConstructor` —
  Result-side types.
- MOVED `attempt`, `withReporting`, `classifyError` from `src/try/`
  to `src/result/`. Internal `AttemptImpl` class moved to
  `src/result/internal/`.
- KEPT as aliases at the top level: `try_`, `tryPromise` (both
  resolve to `fromThrowable` / `fromAsyncThrowable`).
- REMOVED: `Success`, `Failure`, `Try`, the `success` / `failure`
  factories, the `*Try` pipeables, the `_tag: 'Success'` /
  `_tag: 'Failure'` discriminants.

Files:
- src/result/{wrapping,attempt,reporting,classify}.ts (new)
- src/result/types.ts, constants.ts, index.ts (extended)
- src/result/internal/attempt-impl.ts (new, moved from src/try/)
- src/try/index.ts (now a one-file facade re-exporting from result/)
- src/index.ts (root barrel updated)
- src/try/{types,constants,functions,attempt,reporting,classify,
  internal/success-impl, internal/failure-impl,
  internal/attempt-impl}.ts (deleted)
- tests/result/{wrapping,attempt-impl,reporting,classify,index}.test.ts
  (new / moved)
- tests/try/* (deleted)
- docs/internal/product/features/try.md (reframed as a Result
  adapter note)
- docs/internal/product/features/result.md (new "Wrapping Throwing
  Functions" section)

Coverage stays at 100% on lines / branches / functions /
statements. The 24 test files now hold 316 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #439 shipped a Try module and then collapsed it onto Result
through aliasing. This commit goes the rest of the way and removes
the Try facade entirely.

What changed:

- DELETED src/try/ (the directory, including the facade index.ts).
- DELETED docs/internal/product/features/try.md.
- The top-level aliases `try_` and `tryPromise` are gone.
  Consumers wrap throwing code through
  `Result.fromThrowable` / `Result.fromAsyncThrowable` (which
  already return `Result<T, E>`).
- The root barrel no longer imports from `./try/`. All wrapping
  helpers come from `./result/`.
- The product README no longer lists Try as a primitive.
- wrapping.test.ts drops its alias smoke checks; the canonical
  fromThrowable / fromAsyncThrowable tests stay.
- The unify-on-result changeset is rewritten to reflect that the
  Try facade is fully removed.

Coverage stays at 100% on lines / branches / functions /
statements across the 24 test files (314 tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codewizdave
codewizdave merged commit d2a1bc8 into staging Aug 20, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants