Repository: dubzzz/fast-check
Pull request #2960 (2022-05-10 22:39:31)

💥 Remove `frequency` now replaced by `oneof`
With v3, we want to remove some redundant arbitraries like `oneof` vs `frequency`. Both play close roles so instead of having two of them let's just leverage one to reduce as much as possible the API surface for new users and make it easier to get.

Everything done via `frequency` can now be done with `oneof`. Users will just have to change `frequency` to `oneof` for all occurences and tada!

<!-- Context of the PR: short description and potentially linked issues -->

<!-- ...a few words to describe the content of this PR...               -->
<!-- ... -->

<!-- Type of PR: [ ] unchecked / [ ] checked -->

**_Category:_**

- [ ] ✨ Introduce new features
- [ ] 📝 Add or update documentation
- [ ] ✅ Add or update tests
- [ ] 🐛 Fix a bug
- [ ] 🏷️ Add or update types
- [ ] ⚡️ Improve performance
- [ ] _Other(s):_ ...
  <!-- Don't forget to add the gitmoji icon in the name of the PR -->
  <!-- See: https://gitmoji.dev/                                  -->

<!-- Fixing bugs, adding features... may impact existing ones           -->
<!-- in order to track potential issues that could be related to your PR -->
<!-- please check the impacts and describe more precisely what to expect -->

**_Potential impacts:_**

<!-- Generated values: Can your change impact any of the existing generators in terms of generated values, if so which ones? when? -->
<!-- Shrink values:    Can your change impact any of the existing generators in terms of shrink values, if so which ones? when? -->
<!-- Performance:      Can it require some typings changes on user side? Please give more details -->
<!-- Typings:          Is there a potential performance impact? In which cases? -->

- [ ] Generated values
- [ ] Shrink values
- [ ] Performance
- [ ] Typings
- [ ] _Other(s):_ ...


codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/dubzzz/fast-check/pr/2960/builds/248606) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit c3e15e3a893fb32dfb832a3ad0440ec9452430a0:

|Sandbox| Source |
|--|--|
|[Vanilla](https://codesandbox.io/s/vanilla-h073ne)| Configuration |
|[fast-check-examples](https://codesandbox.io/s/fast-check-examples-o37h32)| Configuration |

codecov[bot]: # [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/2960?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) Report
> Merging [#2960](https://codecov.io/gh/dubzzz/fast-check/pull/2960?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) (c3e15e3) into [main](https://codecov.io/gh/dubzzz/fast-check/commit/5a77faac533782cf57004358ab7b37f702f2da1c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) (5a77faa) will **decrease** coverage by `0.01%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##             main    #2960      +/-   ##
==========================================
- Coverage   96.28%   96.27%   -0.02%     
==========================================
  Files         193      192       -1     
  Lines        4522     4509      -13     
  Branches      981      979       -2     
==========================================
- Hits         4354     4341      -13     
  Misses        158      158              
  Partials       10       10              
```

| Flag | Coverage Δ | |
|---|---|---|
| unit-tests | `96.27% <100.00%> (-0.02%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/dubzzz/fast-check/pull/2960?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) | Coverage Δ | |
|---|---|---|
| [src/arbitrary/oneof.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2960/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9vbmVvZi50cw==) | `100.00% <ø> (ø)` | |
| [...bitrary/\_internals/builders/AnyArbitraryBuilder.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2960/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9faW50ZXJuYWxzL2J1aWxkZXJzL0FueUFyYml0cmFyeUJ1aWxkZXIudHM=) | `100.00% <100.00%> (ø)` | |
| [...ternals/builders/CharacterRangeArbitraryBuilder.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2960/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9faW50ZXJuYWxzL2J1aWxkZXJzL0NoYXJhY3RlclJhbmdlQXJiaXRyYXJ5QnVpbGRlci50cw==) | `95.65% <100.00%> (ø)` | |
| [src/arbitrary/lorem.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2960/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9sb3JlbS50cw==) | `100.00% <100.00%> (ø)` | |
| [src/fast-check-default.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2960/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2Zhc3QtY2hlY2stZGVmYXVsdC50cw==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/2960?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/2960?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN). Last update [5a77faa...c3e15e3](https://codecov.io/gh/dubzzz/fast-check/pull/2960?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN).


Patch:
diff --git a/documentation/Arbitraries.md b/documentation/Arbitraries.md
index 4a33836b..06828b3d 100644
--- a/documentation/Arbitraries.md
+++ b/documentation/Arbitraries.md
@@ -2093,18 +2093,20 @@ fc.option(fc.string(), { nil: undefined })
 > Generate one value based on one of the passed arbitraries
 >
 > Randomly chooses an arbitrary at each new generation. Should be provided with at least one arbitrary. Probability to select a specific arbitrary is based on its weight: `weight(instance) / sumOf(weights)` (for depth=0). For higher depths, the probability to select the first arbitrary will increase as we go deeper in the tree so the formula is not applicable as-is. It preserves the shrinking capabilities of the underlying arbitrary. `fc.oneof` is able to shrink inside the failing arbitrary but not across arbitraries (contrary to `fc.constantFrom` when dealing with constant arbitraries) except if called with `withCrossShrink`.
+>
+> **Warning:** The first arbitrary specified on `oneof` will have a privileged position. Constraints like `withCrossShrink` or `depthFactor` tend to favor it over others.
 
 *&#8195;Signatures*
 
 - `fc.oneof(...arbitraries)`
-- `fc.oneof({withCrossShrink?, maxDepth?, depthIdentifier?}, ...arbitraries)`
+- `fc.oneof({withCrossShrink?, maxDepth?, depthFactor?, depthIdentifier?}, ...arbitraries)`
 
 *&#8195;with:*
 
 - `...arbitraries` — _arbitraries that could be used to generate a value. The received instances can either be raw instances of arbitraries (meaning weight is 1) or objects containing the arbitrary and its associated weight (integer value ≥0)_
 - `withCrossShrink?` — default: `false` — _in case of failure the shrinker will try to check if a failure can be found by using the first specified arbitrary. It may be pretty useful for recursive structures as it can easily help reducing their depth in case of failure_
-- `depthFactor?` — default: `undefined` [more](#depth-factor-explained) — _this factor will be used to increase the probability to generate instances of the first passed arbitrary_
 - `maxDepth?` — default: `Number.POSITIVE_INFINITY` — _when reaching maxDepth, the first arbitrary will be used to generate the value_
+- `depthFactor?` — default: `undefined` [more](#depth-factor-explained) — _this factor will be used to increase the probability to generate instances of the first passed arbitrary_
 - `depthIdentifier?` — default: `undefined` — _share the depth between instances using the same `depthIdentifier`_
 
 *&#8195;Usages*
@@ -2138,54 +2140,6 @@ fc.oneof(
 ```
 </details>
 
-<details>
-<summary><b>frequency</b> - [<a href="https://dubzzz.github.io/fast-check/index.html#frequency">api</a>]</summary><br/>
-
-*&#8195;Description*
-
-> Generate one value based on one of the passed arbitraries
->
-> Randomly chooses an arbitrary at each new generation. Should be provided with at least one arbitrary. Probability to select a specific arbitrary is based on its weight, the higher it is the more it will be probable. It preserves the shrinking capabilities of the underlying arbitrary.
->
-> **Warning:** In the upcomping major releases of fast-check the first arbitrary specified on `frequency` will have a privileged position. Constraints like `withCrossShrink` tend to favor it over others and will probably be enabled by default.
-
-*&#8195;Signatures*
-
-- `fc.frequency(...{ arbitrary, weight })`
-- `fc.frequency({withCrossShrink?, maxDepth?, depthIdentifier?}, ...{ arbitrary, weight })`
-
-*&#8195;with:*
-
-- `...{ arbitrary, weight }` — _arbitraries that could be used to generate a value along their weight (the higher the weight, the higher the probability to select this arbitrary will be)_
-- `withCrossShrink?` — default: `false` — _in case of failure the shrinker will try to check if a failure can be found by using the first specified arbitrary (if and only if its weight is strictly greater than 0). It may be pretty useful for recursive structures as it can easily help reducing their depth in case of failure_
-- `depthFactor?` — default: `undefined` [more](#depth-factor-explained) — _this factor will be used to increase the probability to generate instances of the first passed arbitrary if its weight is not zero_
-- `maxDepth?` — default: `Number.POSITIVE_INFINITY` — _when reaching maxDepth, the first arbitrary will be used to generate the value even if its weight is zero_
-- `depthIdentifier?` — default: `undefined` — _share the depth between instances using the same `depthIdentifier`_
-
-*&#8195;Usages*
-
-```js
-fc.frequency(
-  { arbitrary: fc.char(), weight: 2 },
-  { arbitrary: fc.boolean(), weight: 1 }
-)
-// Examples of generated values: true, "&", "8", false, "["…
-
-fc.frequency(
-  { withCrossShrink: true },
-  { arbitrary: fc.boolean(), weight: 2 },
-  { arbitrary: fc.array(fc.boolean()), weight: 1 }
-)
-// Note: In case of failure on an array of boolean values the shrinker will first try to check
-// if a failure can also be triggered with a simple boolean (the first arbitrary specified)
-// if not it will carry on the classical shrinking strategy defined for arrays of boolean.
-// Examples of generated values: true, [true,false], [true,false,false], [false,true,true], [false]…
-
-// fc.frequency fits very well with recursive stuctures built using fc.letrec.
-// Examples of such recursive structures are available with fc.letrec.
-```
-</details>
-
 <details>
 <summary><b>mapToConstant</b> - [<a href="https://dubzzz.github.io/fast-check/index.html#maptoconstant">api</a>]</summary><br/>
 
@@ -3348,7 +3302,7 @@ fc.func(fc.nat())
 const { tree } = fc.letrec(tie => ({
   // Warning: In version 2.x and before, there is no automatic control over the depth of the generated data-structures.
   //   As a consequence to avoid your data-structures to be too deep, it is highly recommended to add the constraint `depthFactor`
-  //   onto your usages of `option`, `oneof` and `frequency` and to put the arbitrary without recursion first.
+  //   onto your usages of `option` and `oneof` and to put the arbitrary without recursion first.
   // In version 3.x, `depthFactor` and `withCrossShrink` will be enabled by default.
   tree: fc.oneof({depthFactor: 'small', withCrossShrink: true}, tie('leaf'), tie('node')),
   node: fc.record({
@@ -3374,7 +3328,7 @@ fc.letrec(tie => ({
     right: fc.option(tie('node'), {maxDepth: 1, depthIdentifier: 'tree'}),
   })
 })).node
-// Note: You can limit the depth of the generated structrures by using the constraint `maxDepth` (see `option`, `oneof` and `frequency`).
+// Note: You can limit the depth of the generated structrures by using the constraint `maxDepth` (see `option` and `oneof`).
 //   On the example above we need to specify `depthIdentifier` to share the depth between left and right branches...
 // Examples of generated values:
 // • {"value":1667728700,"left":{"value":22,"left":null,"right":null},"right":{"value":202444547,"left":null,"right":null}}
@@ -3422,20 +3376,20 @@ fc.letrec(tie => ({
 // • …
 
 fc.letrec(tie => ({
-  tree: fc.frequency({maxDepth: 2}, {arbitrary: tie('leaf'), weight: 0}, {arbitrary: tie('node'), weight: 1}),
+  tree: fc.oneof({maxDepth: 2}, {arbitrary: tie('leaf'), weight: 0}, {arbitrary: tie('node'), weight: 1}),
   node: fc.record({ left: tie('tree'), right: tie('tree') }),
   leaf: fc.nat()
 })).tree
 // Note: Exact depth of 2: not more not less.
-// Note: If you use multiple `option`, `oneof` or `frequency` to define such recursive structure
+// Note: If you use multiple `option` or `oneof` to define such recursive structure
 //   you may want to specify a `depthIdentifier` so that they share the exact same depth.
 //   See examples above for more details.
 // Examples of generated values:
-// • {"left":{"left":2146012257,"right":5},"right":{"left":4,"right":92286720}}
-// • {"left":{"left":17,"right":105221773},"right":{"left":10,"right":1282731170}}
-// • {"left":{"left":289678323,"right":1527632810},"right":{"left":1031855048,"right":861569963}}
-// • {"left":{"left":1001661803,"right":2147483623},"right":{"left":2147483617,"right":729999584}}
-// • {"left":{"left":1517743480,"right":1007350543},"right":{"left":12,"right":2147483643}}
+// • {"left":{"left":517441885,"right":1213144115},"right":{"left":25,"right":782270564}}
+// • {"left":{"left":1192072964,"right":2147483622},"right":{"left":21,"right":20}}
+// • {"left":{"left":415810012,"right":609661118},"right":{"left":7,"right":28}}
+// • {"left":{"left":687315350,"right":633465371},"right":{"left":1404418498,"right":18}}
+// • {"left":{"left":1167552932,"right":16},"right":{"left":22,"right":742062962}}
 // • …
 
 fc.statistics(
@@ -3523,7 +3477,7 @@ fc.statistics(
 > Generate recursive structures
 >
 > ⚠️ Initially `fc.memo` has been designed to offer a higher control over the generated depth. Unfortunately it came with a cost: the arbitrary itself is costly to build.
-> Most of the features offered by `fc.memo` can now be done using `fc.letrec` coupled with `fc.option`, `fc.oneof` or `fc.frequency`.
+> Most of the features offered by `fc.memo` can now be done using `fc.letrec` coupled with `fc.option` or `fc.oneof`.
 > Whenever possible*, we recommend using `fc.letrec` instead of `fc.memo`.
 >
 > *But sometimes it is not possible and `fc.memo` will be the way to go.
@@ -3824,7 +3778,7 @@ Here is how a size translates into depth factors:
 - `xlarge` — `1 / 16`
 
 In the context of fast-check@v2, the condition to leverage an automatic defaulting of the depth factor is to:
-- either define it to `=` for each arbitrary not defaulting it automatically (only `option`, `oneof` and `frequency` do not default it to avoid breaking existing code)
+- either define it to `=` for each arbitrary not defaulting it automatically (only `option` and `oneof` do not default it to avoid breaking existing code)
 - or to configure a `baseSize` in the global settings
 
 If none of these conditions is fulfilled the depth factor will be defaulted to `0` as it was the case befoer we introduced it.
diff --git a/src/arbitrary/_internals/builders/AnyArbitraryBuilder.ts b/src/arbitrary/_internals/builders/AnyArbitraryBuilder.ts
index 8af8e8c9..6e1681ee 100644
--- a/src/arbitrary/_internals/builders/AnyArbitraryBuilder.ts
+++ b/src/arbitrary/_internals/builders/AnyArbitraryBuilder.ts
@@ -2,7 +2,6 @@ import { Arbitrary } from '../../../check/arbitrary/definition/Arbitrary';
 
 import { stringify } from '../../../utils/stringify';
 import { array } from '../../array';
-import { frequency } from '../../frequency';
 import { oneof } from '../../oneof';
 import { tuple } from '../../tuple';
 import { bigInt } from '../../bigInt';
@@ -126,7 +125,7 @@ export function anyArbitraryBuilder(constraints: QualifiedObjectConstraints): Ar
     ),
     // String keys
     keys: constraints.withObjectString
-      ? frequency(
+      ? oneof(
           { arbitrary: constraints.key, weight: 10 },
           { arbitrary: tie('anything').map((o) => stringify(o)), weight: 1 }
         )
diff --git a/src/arbitrary/_internals/builders/CharacterRangeArbitraryBuilder.ts b/src/arbitrary/_internals/builders/CharacterRangeArbitraryBuilder.ts
index 900ca310..31855c98 100644
--- a/src/arbitrary/_internals/builders/CharacterRangeArbitraryBuilder.ts
+++ b/src/arbitrary/_internals/builders/CharacterRangeArbitraryBuilder.ts
@@ -1,6 +1,6 @@
 import { fullUnicode } from '../../fullUnicode';
 import { Arbitrary } from '../../../check/arbitrary/definition/Arbitrary';
-import { frequency } from '../../frequency';
+import { oneof } from '../../oneof';
 import { mapToConstant } from '../../mapToConstant';
 
 /** @internal */
@@ -43,4 +43,4 @@ export const buildAlphaNumericArbitrary = (others: string[]): Arbitrary<string>
 
 /** @internal */
 export const buildAlphaNumericPercentArbitrary = (others: string[]): Arbitrary<string> =>
-  frequency({ weight: 10, arbitrary: buildAlphaNumericArbitrary(others) }, { weight: 1, arbitrary: percentCharArb });
+  oneof({ weight: 10, arbitrary: buildAlphaNumericArbitrary(others) }, { weight: 1, arbitrary: percentCharArb });
diff --git a/src/arbitrary/frequency.ts b/src/arbitrary/frequency.ts
deleted file mode 100644
index f2466baf..00000000
--- a/src/arbitrary/frequency.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
-import { FrequencyArbitrary } from './_internals/FrequencyArbitrary';
-import { DepthIdentifier } from './_internals/helpers/DepthContext';
-import { DepthFactorSizeForArbitrary } from './_internals/helpers/MaxLengthFromMinLength';
-
-/**
- * Conjonction of a weight and an arbitrary used by {@link frequency}
- * in order to generate values
- *
- * @remarks Since 1.18.0
- * @public
- */
-export interface WeightedArbitrary<T> {
-  /**
-   * Weight to be applied when selecting which arbitrary should be used
-   * @remarks Since 0.0.7
-   */
-  weight: number;
-  /**
-   * Instance of Arbitrary
-   * @remarks Since 0.0.7
-   */
-  arbitrary: Arbitrary<T>;
-}
-
-/**
- * Infer the type of the Arbitrary produced by {@link frequency}
- * given the type of the source arbitraries
- *
- * @remarks Since 2.2.0
- * @public
- */
-export type FrequencyValue<Ts extends WeightedArbitrary<unknown>[]> = {
-  [K in keyof Ts]: Ts[K] extends WeightedArbitrary<infer U> ? U : never;
-}[number];
-
-/**
- * Constraints to be applied on {@link frequency}
- * @remarks Since 2.14.0
- * @public
- */
-export type FrequencyContraints = {
-  /**
-   * When set to true, the shrinker of frequency will try to check if the first arbitrary
-   * could have been used to discover an issue. It allows to shrink trees.
-   *
-   * Warning: First arbitrary must be the one resulting in the smallest structures
-   * for usages in deep tree-like structures.
-   *
-   * Warning: First arbitrary will not be used if its weight is set to zero.
-   *
-   * @remarks Since 2.14.0
-   */
-  withCrossShrink?: boolean;
-  /**
-   * While going deeper and deeper within a recursive structure (see {@link letrec}),
-   * this factor will be used to increase the probability to generate instances
-   * of the first passed arbitrary.
-   *
-   * Warning: First arbitrary will not be used if its weight is set to zero.
-   *
-   * @remarks Since 2.14.0
-   */
-  depthFactor?: DepthFactorSizeForArbitrary;
-  /**
-   * Maximal authorized depth.
-   * Once this depth has been reached only the first arbitrary will be used.
-   *
-   * Warning: Contrary to others, first arbitrary will be used even if its weight is set to zero.
-   *
-   * @remarks Since 2.14.0
-   */
-  maxDepth?: number;
-  /**
-   * Depth identifier can be used to share the current depth between several instances.
-   *
-   * By default, if not specified, each instance of frequency will have its own depth.
-   * In other words: you can have depth=1 in one while you have depth=100 in another one.
-   *
-   * @remarks Since 2.14.0
-   */
-  depthIdentifier?: DepthIdentifier | string;
-};
-
-/** @internal */
-function isFrequencyContraints(
-  param: FrequencyContraints | WeightedArbitrary<unknown> | undefined
-): param is FrequencyContraints {
-  return param != null && typeof param === 'object' && !('arbitrary' in param);
-}
-
-/**
- * For one of the values generated by `...warbs` - the probability of selecting the ith warb is of `warb[i].weight / sum(warb[j].weight)`
- *
- * **WARNING**: It expects at least one (Arbitrary, weight)
- *
- * @param warbs - (Arbitrary, weight)s that might be called to produce a value
- *
- * @remarks Since 0.0.7
- * @public
- */
-function frequency<Ts extends WeightedArbitrary<unknown>[]>(...warbs: Ts): Arbitrary<FrequencyValue<Ts>>;
-/**
- * For one of the values generated by `...warbs` - the probability of selecting the ith warb is of `warb[i].weight / sum(warb[j].weight)`
- *
- * **WARNING**: It expects at least one (Arbitrary, weight)
- *
- * @param constraints - Constraints to be applied when generating the values
- * @param warbs - (Arbitrary, weight)s that might be called to produce a value
- *
- * @remarks Since 0.0.7
- * @public
- */
-function frequency<Ts extends WeightedArbitrary<unknown>[]>(
-  constraints: FrequencyContraints,
-  ...warbs: Ts
-): Arbitrary<FrequencyValue<Ts>>;
-function frequency<Ts extends WeightedArbitrary<unknown>[]>(
-  ...args: [...Ts] | [FrequencyContraints, ...Ts]
-): Arbitrary<FrequencyValue<Ts>> {
-  // TODO With TypeScript 4.0 it will be possible to properly define typings for `frequency(...arbs, constraints)`
-  const label = 'fc.frequency';
-  const constraints = args[0];
-  if (isFrequencyContraints(constraints)) {
-    return FrequencyArbitrary.from(args.slice(1) as WeightedArbitrary<FrequencyValue<Ts>>[], constraints, label);
-  }
-
-  return FrequencyArbitrary.from(args as WeightedArbitrary<FrequencyValue<Ts>>[], {}, label);
-}
-export { frequency };
diff --git a/src/arbitrary/lorem.ts b/src/arbitrary/lorem.ts
index b4b8da01..a9226afe 100644
--- a/src/arbitrary/lorem.ts
+++ b/src/arbitrary/lorem.ts
@@ -1,7 +1,7 @@
 import { array } from './array';
 import { constant } from './constant';
 import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
-import { frequency } from './frequency';
+import { MaybeWeightedArbitrary, oneof } from './oneof';
 import {
   sentencesToParagraphMapper,
   sentencesToParagraphUnmapper,
@@ -44,10 +44,10 @@ export interface LoremConstraints {
 }
 
 /**
- * Helper function responsible to build the entries for frequency
+ * Helper function responsible to build the entries for oneof
  * @internal
  */
-const h = (v: string, w: number) => {
+const h = (v: string, w: number): MaybeWeightedArbitrary<string> => {
   return { arbitrary: constant(v), weight: w };
 };
 
@@ -72,7 +72,7 @@ const h = (v: string, w: number) => {
  * @internal
  */
 function loremWord() {
-  return frequency(
+  return oneof(
     h('non', 6),
     h('adipiscing', 5),
     h('ligula', 5),
diff --git a/src/arbitrary/oneof.ts b/src/arbitrary/oneof.ts
index 7ca47d1a..9f00d1e8 100644
--- a/src/arbitrary/oneof.ts
+++ b/src/arbitrary/oneof.ts
@@ -1,9 +1,28 @@
 import { Arbitrary, isArbitrary } from '../check/arbitrary/definition/Arbitrary';
-import { WeightedArbitrary } from './frequency';
 import { FrequencyArbitrary } from './_internals/FrequencyArbitrary';
 import { DepthIdentifier } from './_internals/helpers/DepthContext';
 import { DepthFactorSizeForArbitrary } from './_internals/helpers/MaxLengthFromMinLength';
 
+/**
+ * Conjonction of a weight and an arbitrary used by {@link oneof}
+ * in order to generate values
+ *
+ * @remarks Since 1.18.0
+ * @public
+ */
+export interface WeightedArbitrary<T> {
+  /**
+   * Weight to be applied when selecting which arbitrary should be used
+   * @remarks Since 0.0.7
+   */
+  weight: number;
+  /**
+   * Instance of Arbitrary
+   * @remarks Since 0.0.7
+   */
+  arbitrary: Arbitrary<T>;
+}
+
 /**
  * Either an `Arbitrary<T>` or a `WeightedArbitrary<T>`
  * @remarks Since 3.0.0
diff --git a/src/check/property/AsyncProperty.generic.ts b/src/check/property/AsyncProperty.generic.ts
index 04e99840..eff9d1ef 100644
--- a/src/check/property/AsyncProperty.generic.ts
+++ b/src/check/property/AsyncProperty.generic.ts
@@ -1,7 +1,7 @@
 import { Random } from '../../random/generator/Random';
 import { Arbitrary } from '../arbitrary/definition/Arbitrary';
 import { PreconditionFailure } from '../precondition/PreconditionFailure';
-import { IRawProperty, runIdToFrequency } from './IRawProperty';
+import { PropertyFailure, IRawProperty, runIdToFrequency } from './IRawProperty';
 import { readConfigureGlobal, GlobalAsyncPropertyHookFunction } from '../runner/configuration/GlobalParameters';
 import { Value } from '../arbitrary/definition/Value';
 import { Stream } from '../../stream/Stream';
@@ -100,17 +100,21 @@ export class AsyncProperty<Ts> implements IAsyncPropertyWithHooks<Ts> {
     return this.arb.shrink(value.value_, safeContext).map(noUndefinedAsContext);
   }
 
-  async run(v: Ts): Promise<PreconditionFailure | string | null> {
+  async run(v: Ts): Promise<PreconditionFailure | PropertyFailure | null> {
     await this.beforeEachHook();
     try {
       const output = await this.predicate(v);
-      return output == null || output === true ? null : 'Property failed by returning false';
+      return output == null || output === true
+        ? null
+        : { error: undefined, errorMessage: 'Property failed by returning false' };
     } catch (err) {
       // precondition failure considered as success for the first version
       if (PreconditionFailure.isFailure(err)) return err;
-      // exception as string in case of real failure
-      if (err instanceof Error && err.stack) return `${err}\n\nStack trace: ${err.stack}`;
-      return `${err}`;
+      // exception as PropertyFailure in case of real failure
+      if (err instanceof Error && err.stack) {
+        return { error: err, errorMessage: `${err}\n\nStack trace: ${err.stack}` };
+      }
+      return { error: err, errorMessage: `${err}` };
     } finally {
       await this.afterEachHook();
     }
diff --git a/src/check/property/IRawProperty.ts b/src/check/property/IRawProperty.ts
index 1d6b6bb0..05f8046e 100644
--- a/src/check/property/IRawProperty.ts
+++ b/src/check/property/IRawProperty.ts
@@ -3,6 +3,25 @@ import { Stream } from '../../stream/Stream';
 import { Value } from '../arbitrary/definition/Value';
 import { PreconditionFailure } from '../precondition/PreconditionFailure';
 
+/**
+ * Represent failures of the property
+ * @remarks Since 3.0.0
+ * @public
+ */
+export type PropertyFailure = {
+  /**
+   * The original error that has been intercepted.
+   * Possibly not an instance Error as users can throw anything.
+   * @remarks Since 3.0.0
+   */
+  error: unknown;
+  /**
+   * The error message extracted from the error
+   * @remarks Since 3.0.0
+   */
+  errorMessage: string;
+};
+
 /**
  * Property
  *
@@ -49,8 +68,8 @@ export interface IRawProperty<Ts, IsAsync extends boolean = boolean> {
   run(
     v: Ts
   ):
-    | (IsAsync extends true ? Promise<PreconditionFailure | string | null> : never)
-    | (IsAsync extends false ? PreconditionFailure | string | null : never);
+    | (IsAsync extends true ? Promise<PreconditionFailure | PropertyFailure | null> : never)
+    | (IsAsync extends false ? PreconditionFailure | PropertyFailure | null : never);
 }
 
 /**
diff --git a/src/check/property/Property.generic.ts b/src/check/property/Property.generic.ts
index 4b7a30c9..b195eeb2 100644
--- a/src/check/property/Property.generic.ts
+++ b/src/check/property/Property.generic.ts
@@ -1,7 +1,7 @@
 import { Random } from '../../random/generator/Random';
 import { Arbitrary } from '../arbitrary/definition/Arbitrary';
 import { PreconditionFailure } from '../precondition/PreconditionFailure';
-import { IRawProperty, runIdToFrequency } from './IRawProperty';
+import { PropertyFailure, IRawProperty, runIdToFrequency } from './IRawProperty';
 import { readConfigureGlobal, GlobalPropertyHookFunction } from '../runner/configuration/GlobalParameters';
 import { Value } from '../arbitrary/definition/Value';
 import { Stream } from '../../stream/Stream';
@@ -116,17 +116,21 @@ export class Property<Ts> implements IProperty<Ts>, IPropertyWithHooks<Ts> {
     return this.arb.shrink(value.value_, safeContext).map(noUndefinedAsContext);
   }
 
-  run(v: Ts): PreconditionFailure | string | null {
+  run(v: Ts): PreconditionFailure | PropertyFailure | null {
     this.beforeEachHook();
     try {
       const output = this.predicate(v);
-      return output == null || output === true ? null : 'Property failed by returning false';
+      return output == null || output === true
+        ? null
+        : { error: undefined, errorMessage: 'Property failed by returning false' };
     } catch (err) {
       // precondition failure considered as success for the first version
       if (PreconditionFailure.isFailure(err)) return err;
-      // exception as string in case of real failure
-      if (err instanceof Error && err.stack) return `${err}\n\nStack trace: ${err.stack}`;
-      return `${err}`;
+      // exception as PropertyFailure in case of real failure
+      if (err instanceof Error && err.stack) {
+        return { error: err, errorMessage: `${err}\n\nStack trace: ${err.stack}` };
+      }
+      return { error: err, errorMessage: `${err}` };
     } finally {
       this.afterEachHook();
     }
diff --git a/src/check/property/TimeoutProperty.ts b/src/check/property/TimeoutProperty.ts
index 47e773e5..2bc0a55c 100644
--- a/src/check/property/TimeoutProperty.ts
+++ b/src/check/property/TimeoutProperty.ts
@@ -2,14 +2,14 @@ import { Random } from '../../random/generator/Random';
 import { Stream } from '../../stream/Stream';
 import { Value } from '../arbitrary/definition/Value';
 import { PreconditionFailure } from '../precondition/PreconditionFailure';
-import { IRawProperty } from './IRawProperty';
+import { PropertyFailure, IRawProperty } from './IRawProperty';
 
 /** @internal */
 const timeoutAfter = (timeMs: number) => {
   let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
-  const promise = new Promise<string>((resolve) => {
+  const promise = new Promise<PropertyFailure>((resolve) => {
     timeoutHandle = setTimeout(() => {
-      resolve(`Property timeout: exceeded limit of ${timeMs} milliseconds`);
+      resolve({ error: undefined, errorMessage: `Property timeout: exceeded limit of ${timeMs} milliseconds` });
     }, timeMs);
   });
   return {
@@ -36,7 +36,7 @@ export class TimeoutProperty<Ts> implements IRawProperty<Ts, true> {
     return this.property.shrink(value);
   }
 
-  async run(v: Ts): Promise<string | PreconditionFailure | null> {
+  async run(v: Ts): Promise<PreconditionFailure | PropertyFailure | null> {
     const t = timeoutAfter(this.timeMs);
     const propRun = Promise.race([this.property.run(v), t.promise]);
     propRun.then(t.clear, t.clear); // always clear timeout handle - catch should never occur
diff --git a/src/check/runner/Runner.ts b/src/check/runner/Runner.ts
index c1ee7097..acd3ff50 100644
--- a/src/check/runner/Runner.ts
+++ b/src/check/runner/Runner.ts
@@ -1,6 +1,6 @@
 import { Stream, stream } from '../../stream/Stream';
 import { PreconditionFailure } from '../precondition/PreconditionFailure';
-import { IRawProperty } from '../property/IRawProperty';
+import { PropertyFailure, IRawProperty } from '../property/IRawProperty';
 import { readConfigureGlobal } from './configuration/GlobalParameters';
 import { Parameters } from './configuration/Parameters';
 import { QualifiedParameters } from './configuration/QualifiedParameters';
@@ -27,7 +27,7 @@ function runIt<Ts>(
 ): RunExecution<Ts> {
   const runner = new RunnerIterator(sourceValues, shrink, verbose, interruptedAsFailure);
   for (const v of runner) {
-    const out = property.run(v) as PreconditionFailure | string | null;
+    const out = property.run(v) as PreconditionFailure | PropertyFailure | null;
     runner.handleResult(out);
   }
   return runner.runExecution;
diff --git a/src/check/runner/RunnerIterator.ts b/src/check/runner/RunnerIterator.ts
index 9767f937..9d0316af 100644
--- a/src/check/runner/RunnerIterator.ts
+++ b/src/check/runner/RunnerIterator.ts
@@ -1,5 +1,6 @@
 import { Value } from '../arbitrary/definition/Value';
 import { PreconditionFailure } from '../precondition/PreconditionFailure';
+import { PropertyFailure } from '../property/IRawProperty';
 import { VerbosityLevel } from './configuration/VerbosityLevel';
 import { RunExecution } from './reporter/RunExecution';
 import { SourceValuesIterator } from './SourceValuesIterator';
@@ -41,14 +42,14 @@ export class RunnerIterator<Ts> implements IterableIterator<Ts> {
     ++this.currentIdx;
     return { done: false, value: nextValue.value.value_ };
   }
-  handleResult(result: PreconditionFailure | string | null): void {
+  handleResult(result: PreconditionFailure | PropertyFailure | null): void {
     // WARNING: This function has to be called after a call to next
     //          Otherwise it will not be able to execute with the right currentShrinkable (or crash)
     // As a consequence: currentShrinkable is always defined in the code below
-    if (result != null && typeof result === 'string') {
+    if (result != null && typeof result === 'object' && !PreconditionFailure.isFailure(result)) {
       // failed run
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      this.runExecution.fail(this.currentValue!.value_, this.currentIdx, result);
+      this.runExecution.fail(this.currentValue!.value_, this.currentIdx, result.errorMessage);
       this.currentIdx = -1;
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       this.nextValues = this.shrink(this.currentValue!);
diff --git a/src/fast-check-default.ts b/src/fast-check-default.ts
index 23bbce6e..e4d05f82 100644
--- a/src/fast-check-default.ts
+++ b/src/fast-check-default.ts
@@ -6,7 +6,7 @@ import {
   AsyncPropertyHookFunction,
 } from './check/property/AsyncProperty';
 import { property, IProperty, IPropertyWithHooks, PropertyHookFunction } from './check/property/Property';
-import { IRawProperty } from './check/property/IRawProperty';
+import { IRawProperty, PropertyFailure } from './check/property/IRawProperty';
 import { Parameters } from './check/runner/configuration/Parameters';
 import {
   RunDetails,
@@ -42,7 +42,6 @@ import { dictionary, DictionaryConstraints } from './arbitrary/dictionary';
 import { emailAddress, EmailAddressConstraints } from './arbitrary/emailAddress';
 import { double, DoubleConstraints } from './arbitrary/double';
 import { float, FloatConstraints } from './arbitrary/float';
-import { frequency, WeightedArbitrary, FrequencyValue, FrequencyContraints } from './arbitrary/frequency';
 import { compareBooleanFunc } from './arbitrary/compareBooleanFunc';
 import { compareFunc } from './arbitrary/compareFunc';
 import { func } from './arbitrary/func';
@@ -65,7 +64,7 @@ import { anything } from './arbitrary/anything';
 import { unicodeJsonValue } from './arbitrary/unicodeJsonValue';
 import { jsonValue, JsonValue } from './arbitrary/jsonValue';
 import { unicodeJson } from './arbitrary/unicodeJson';
-import { oneof, OneOfValue, OneOfConstraints, MaybeWeightedArbitrary } from './arbitrary/oneof';
+import { oneof, OneOfValue, OneOfConstraints, MaybeWeightedArbitrary, WeightedArbitrary } from './arbitrary/oneof';
 import { option, OptionConstraints } from './arbitrary/option';
 import { record, RecordConstraints, RecordValue } from './arbitrary/record';
 import {
@@ -221,6 +220,7 @@ export {
   IAsyncPropertyWithHooks,
   AsyncPropertyHookFunction,
   PropertyHookFunction,
+  PropertyFailure,
   // pre-built arbitraries
   boolean,
   falsy,
@@ -256,7 +256,6 @@ export {
   mapToConstant,
   option,
   oneof,
-  frequency,
   clone,
   shuffledSubarray,
   subarray,
@@ -352,7 +351,6 @@ export {
   Float32ArrayConstraints,
   Float64ArrayConstraints,
   FloatConstraints,
-  FrequencyContraints,
   IntArrayConstraints,
   IntegerConstraints,
   JsonSharedConstraints,
@@ -384,7 +382,6 @@ export {
   CloneValue,
   ContextValue,
   FalsyValue,
-  FrequencyValue,
   JsonValue,
   OneOfValue,
   RecordValue,


Test patch:
diff --git a/test/e2e/RecursiveStructures.spec.ts b/test/e2e/RecursiveStructures.spec.ts
index c2d77d24..54525b76 100644
--- a/test/e2e/RecursiveStructures.spec.ts
+++ b/test/e2e/RecursiveStructures.spec.ts
@@ -6,25 +6,6 @@ describe(`RecursiveStructures (seed: ${seed})`, () => {
   // >  The arbitrary used as base-case for the recursive structure is a constant.
   // >  It either pass or fail. But we ca never geenrate the wrong one and miss shrinking opportunities.
 
-  it('Should shrink letrec/frequency towards the smallest case (on very simple scenario)', () => {
-    // Arrange
-    const failingLength = 2;
-    const dataArb = fc.letrec((tie) => ({
-      data: fc.frequency(
-        { withCrossShrink: true },
-        { arbitrary: fc.constant([0]), weight: 1 },
-        { arbitrary: fc.tuple(tie('data'), tie('data')), weight: 1 }
-      ),
-    })).data;
-
-    // Act
-    const out = fc.check(fc.property(dataArb, (data) => flat(data).length < failingLength));
-
-    // Assert
-    expect(out.failed).toBe(true);
-    expect(flat(out.counterexample![0])).toHaveLength(failingLength);
-  });
-
   it('Should shrink letrec/oneof towards the smallest case (on very simple scenario)', () => {
     // Arrange
     const failingLength = 2;
@@ -55,27 +36,6 @@ describe(`RecursiveStructures (seed: ${seed})`, () => {
     expect(flat(out.counterexample![0])).toHaveLength(failingLength);
   });
 
-  it('Should shrink memo/frequency towards the smallest case (on very simple scenario)', () => {
-    // Arrange
-    const failingLength = 2;
-    const dataArb: fc.Memo<unknown[]> = fc.memo((n) => {
-      if (n <= 1) return fc.constant([0]);
-      else
-        return fc.frequency(
-          { withCrossShrink: true },
-          { arbitrary: fc.constant([0]), weight: 1 },
-          { arbitrary: fc.tuple(dataArb(), dataArb()), weight: 1 }
-        );
-    });
-
-    // Act
-    const out = fc.check(fc.property(dataArb(5), (data) => flat(data).length < failingLength));
-
-    // Assert
-    expect(out.failed).toBe(true);
-    expect(flat(out.counterexample![0])).toHaveLength(failingLength);
-  });
-
   it('Should shrink memo/oneof towards the smallest case (on very simple scenario)', () => {
     // Arrange
     const failingLength = 2;
diff --git a/test/e2e/StateFullArbitraries.spec.ts b/test/e2e/StateFullArbitraries.spec.ts
index 555cf56f..3ba95c45 100644
--- a/test/e2e/StateFullArbitraries.spec.ts
+++ b/test/e2e/StateFullArbitraries.spec.ts
@@ -76,20 +76,6 @@ describe(`StateFullArbitraries (seed: ${seed})`, () => {
       expect(nonClonedDetected).toBe(false);
       expect(status.counterexample![1].size()).toEqual(1);
     });
-    it('fc.frequency', () => {
-      let nonClonedDetected = false;
-      const status = fc.check(
-        fc.property(fc.integer(), fc.frequency({ weight: 1, arbitrary: fc.context() }), fc.integer(), (a, ctx, b) => {
-          nonClonedDetected = nonClonedDetected || ctx.size() !== 0;
-          ctx.log('logging stuff');
-          return a < b;
-        }),
-        { seed }
-      );
-      expect(status.failed).toBe(true);
-      expect(nonClonedDetected).toBe(false);
-      expect(status.counterexample![1].size()).toEqual(1);
-    });
     it('fc.option', () => {
       let nonClonedDetected = false;
       const status = fc.check(
diff --git a/test/type/main.ts b/test/type/main.ts
index c35ac1d8..b7896f98 100644
--- a/test/type/main.ts
+++ b/test/type/main.ts
@@ -321,26 +321,6 @@ fc.oneof(fc.string(), '1');
 // @ts-expect-error - oneof expects weighted arbitraries with real arbitrraies and not raw values
 fc.oneof({ arbitrary: fc.string(), weight: 1 }, { arbitrary: '1', weight: 1 });
 
-// frequency arbitrary
-expectType<fc.Arbitrary<string>>()(
-  fc.frequency({ arbitrary: fc.string(), weight: 1 }, { arbitrary: fc.fullUnicodeString(), weight: 1 }),
-  '"frequency" with multiple arguments having the same type'
-);
-expectType<fc.Arbitrary<number | string>>()(
-  fc.frequency({ arbitrary: fc.string(), weight: 1 }, { arbitrary: fc.nat(), weight: 1 }),
-  '"frequency" with multiple arguments of different types'
-);
-expectType<fc.Arbitrary<string | number>>()(
-  fc.frequency({}, { arbitrary: fc.string(), weight: 1 }, { arbitrary: fc.nat(), weight: 1 }),
-  '"frequency" with different types and empty constraints'
-);
-expectType<fc.Arbitrary<string | number>>()(
-  fc.frequency({ withCrossShrink: true }, { arbitrary: fc.string(), weight: 1 }, { arbitrary: fc.nat(), weight: 1 }),
-  '"frequency" with different types and some constraints'
-);
-// @ts-expect-error - frequency expects arbitraries not raw values
-fc.frequency({ arbitrary: fc.string(), weight: 1 }, { arbitrary: '1', weight: 1 });
-
 // option arbitrary
 expectType<fc.Arbitrary<number | null>>()(fc.option(fc.nat()), '"option" without any constraints');
 expectType<fc.Arbitrary<number | null>>()(
diff --git a/test/unit/arbitrary/frequency.spec.ts b/test/unit/arbitrary/frequency.spec.ts
deleted file mode 100644
index 6582f983..00000000
--- a/test/unit/arbitrary/frequency.spec.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import * as fc from '../../../lib/fast-check';
-import { frequency, FrequencyContraints } from '../../../src/arbitrary/frequency';
-import { fakeArbitrary } from './__test-helpers__/ArbitraryHelpers';
-import * as FrequencyArbitraryMock from '../../../src/arbitrary/_internals/FrequencyArbitrary';
-import { sizeArb } from './__test-helpers__/SizeHelpers';
-
-function beforeEachHook() {
-  jest.resetModules();
-  jest.restoreAllMocks();
-  fc.configureGlobal({ beforeEach: beforeEachHook });
-}
-beforeEach(beforeEachHook);
-
-describe('frequency', () => {
-  it('should call FrequencyArbitrary.from with the right parameters when called with constraints', () =>
-    fc.assert(
-      fc.property(
-        fc.record(
-          {
-            withCrossShrink: fc.boolean(),
-            depthIdentifier: fc.string(),
-            depthFactor: fc.oneof(fc.double({ min: 0.01, noDefaultInfinity: true, noNaN: true }), sizeArb),
-            maxDepth: fc.nat(),
-          },
-          { requiredKeys: [] }
-        ),
-        (constraints: Partial<FrequencyContraints>) => {
-          // Arrange
-          const expectedArb = fakeArbitrary().instance;
-          const from = jest.spyOn(FrequencyArbitraryMock.FrequencyArbitrary, 'from');
-          from.mockReturnValue(expectedArb);
-          const { instance: arb1 } = fakeArbitrary();
-          const { instance: arb2 } = fakeArbitrary();
-          const weight1 = 10;
-          const weight2 = 3;
-
-          // Act
-          const out = frequency(
-            constraints,
-            { arbitrary: arb1, weight: weight1 },
-            { arbitrary: arb2, weight: weight2 }
-          );
-
-          // Assert
-          expect(from).toHaveBeenCalledWith(
-            [
-              { arbitrary: arb1, weight: weight1 },
-              { arbitrary: arb2, weight: weight2 },
-            ],
-            constraints,
-            'fc.frequency'
-          );
-          expect(out).toBe(expectedArb);
-        }
-      )
-    ));
-
-  it('should call FrequencyArbitrary.from with the right parameters when called without constraints', () => {
-    // Arrange
-    const expectedArb = fakeArbitrary().instance;
-    const from = jest.spyOn(FrequencyArbitraryMock.FrequencyArbitrary, 'from');
-    from.mockReturnValue(expectedArb);
-    const { instance: arb1 } = fakeArbitrary();
-    const { instance: arb2 } = fakeArbitrary();
-    const weight1 = 10;
-    const weight2 = 3;
-
-    // Act
-    const out = frequency({ arbitrary: arb1, weight: weight1 }, { arbitrary: arb2, weight: weight2 });
-
-    // Assert
-    expect(from).toHaveBeenCalledWith(
-      [
-        { arbitrary: arb1, weight: weight1 },
-        { arbitrary: arb2, weight: weight2 },
-      ],
-      {},
-      'fc.frequency'
-    );
-    expect(out).toBe(expectedArb);
-  });
-});
diff --git a/test/unit/check/property/AsyncProperty.spec.ts b/test/unit/check/property/AsyncProperty.spec.ts
index af7c7c77..b1b1f98f 100644
--- a/test/unit/check/property/AsyncProperty.spec.ts
+++ b/test/unit/check/property/AsyncProperty.spec.ts
@@ -9,6 +9,7 @@ import * as stubRng from '../../stubs/generators';
 import { Value } from '../../../../src/check/arbitrary/definition/Value';
 import { fakeArbitrary } from '../../arbitrary/__test-helpers__/ArbitraryHelpers';
 import { Stream } from '../../../../src/stream/Stream';
+import { PropertyFailure } from '../../../../src/check/property/IRawProperty';
 
 describe('AsyncProperty', () => {
   afterEach(() => resetConfigureGlobal());
@@ -23,15 +24,21 @@ describe('AsyncProperty', () => {
     const p = asyncProperty(stubArb.single(8), async (_arg: number) => {
       throw 'predicate throws';
     });
-    expect(await p.run(p.generate(stubRng.mutable.nocall()).value)).toEqual('predicate throws');
+    expect(await p.run(p.generate(stubRng.mutable.nocall()).value)).toEqual({
+      error: 'predicate throws', // the original error is a string in this test
+      errorMessage: 'predicate throws', // the original error results in this message
+    });
   });
   it('Should fail if predicate throws an Error', async () => {
+    let originalError: Error | null = null;
     const p = asyncProperty(stubArb.single(8), async (_arg: number) => {
-      throw new Error('predicate throws');
+      originalError = new Error('predicate throws');
+      throw originalError;
     });
     const out = await p.run(p.generate(stubRng.mutable.nocall()).value);
-    expect(out).toContain('predicate throws');
-    expect(out).toContain('\n\nStack trace:');
+    expect((out as PropertyFailure).errorMessage).toContain('predicate throws');
+    expect((out as PropertyFailure).errorMessage).toContain('\n\nStack trace:');
+    expect((out as PropertyFailure).error).toBe(originalError);
   });
   it('Should forward failure of runs with failing precondition', async () => {
     let doNotResetThisValue = false;
diff --git a/test/unit/check/property/Property.spec.ts b/test/unit/check/property/Property.spec.ts
index 22adfdcb..078fefe1 100644
--- a/test/unit/check/property/Property.spec.ts
+++ b/test/unit/check/property/Property.spec.ts
@@ -9,6 +9,7 @@ import * as stubRng from '../../stubs/generators';
 import { fakeArbitrary } from '../../arbitrary/__test-helpers__/ArbitraryHelpers';
 import { Value } from '../../../../src/check/arbitrary/definition/Value';
 import { Stream } from '../../../../src/stream/Stream';
+import { PropertyFailure } from '../../../../src/check/property/IRawProperty';
 
 describe('Property', () => {
   afterEach(() => resetConfigureGlobal());
@@ -24,15 +25,21 @@ describe('Property', () => {
       throw 'predicate throws';
     });
     const out = p.run(p.generate(stubRng.mutable.nocall()).value);
-    expect(out).toEqual('predicate throws');
+    expect(out).toEqual({
+      error: 'predicate throws', // the original error is a string in this test
+      errorMessage: 'predicate throws', // the original error results in this message
+    });
   });
   it('Should fail if predicate throws an Error', () => {
+    let originalError: Error | null = null;
     const p = property(stubArb.single(8), (_arg: number) => {
-      throw new Error('predicate throws');
+      originalError = new Error('predicate throws');
+      throw originalError;
     });
     const out = p.run(p.generate(stubRng.mutable.nocall()).value);
-    expect(out).toContain('predicate throws');
-    expect(out).toContain('\n\nStack trace:');
+    expect((out as PropertyFailure).errorMessage).toContain('predicate throws');
+    expect((out as PropertyFailure).errorMessage).toContain('\n\nStack trace:');
+    expect((out as PropertyFailure).error).toBe(originalError);
   });
   it('Should forward failure of runs with failing precondition', async () => {
     let doNotResetThisValue = false;
diff --git a/test/unit/check/property/TimeoutProperty.spec.ts b/test/unit/check/property/TimeoutProperty.spec.ts
index 8ff0c9f4..1f20c7bb 100644
--- a/test/unit/check/property/TimeoutProperty.spec.ts
+++ b/test/unit/check/property/TimeoutProperty.spec.ts
@@ -66,12 +66,13 @@ describe('TimeoutProperty', () => {
 
   it('should not timeout if it fails in time', async () => {
     // Arrange
+    const errorFromUnderlying = { error: undefined, errorMessage: 'plop' };
     jest.useFakeTimers();
     const { instance: decoratedProperty, run } = fakeProperty(true);
     run.mockReturnValueOnce(
       new Promise(function (resolve) {
         // underlying property is not supposed to throw (reject)
-        setTimeout(() => resolve('plop'), 10);
+        setTimeout(() => resolve(errorFromUnderlying), 10);
       })
     );
 
@@ -82,7 +83,7 @@ describe('TimeoutProperty', () => {
     await runPromise;
 
     // Assert
-    expect(await runPromise).toBe('plop');
+    expect(await runPromise).toBe(errorFromUnderlying);
   });
 
   it('should clear all started timeouts on success', async () => {
@@ -104,11 +105,12 @@ describe('TimeoutProperty', () => {
 
   it('should clear all started timeouts on failure', async () => {
     // Arrange
+    const errorFromUnderlying = { error: undefined, errorMessage: 'plop' };
     jest.useFakeTimers();
     jest.spyOn(global, 'setTimeout');
     jest.spyOn(global, 'clearTimeout');
     const { instance: decoratedProperty, run } = fakeProperty(true);
-    run.mockResolvedValueOnce('plop');
+    run.mockResolvedValueOnce(errorFromUnderlying);
 
     // Act
     const timeoutProp = new TimeoutProperty(decoratedProperty, 100);
@@ -135,7 +137,10 @@ describe('TimeoutProperty', () => {
     jest.advanceTimersByTime(10);
 
     // Assert
-    expect(await runPromise).toEqual(`Property timeout: exceeded limit of 10 milliseconds`);
+    expect(await runPromise).toEqual({
+      error: undefined,
+      errorMessage: `Property timeout: exceeded limit of 10 milliseconds`,
+    });
   });
 
   it('Should timeout if it never ends', async () => {
@@ -150,6 +155,9 @@ describe('TimeoutProperty', () => {
     jest.advanceTimersByTime(10);
 
     // Assert
-    expect(await runPromise).toEqual(`Property timeout: exceeded limit of 10 milliseconds`);
+    expect(await runPromise).toEqual({
+      error: undefined,
+      errorMessage: `Property timeout: exceeded limit of 10 milliseconds`,
+    });
   });
 });
diff --git a/test/unit/check/runner/Runner.spec.ts b/test/unit/check/runner/Runner.spec.ts
index aa580145..9f09325d 100644
--- a/test/unit/check/runner/Runner.spec.ts
+++ b/test/unit/check/runner/Runner.spec.ts
@@ -168,7 +168,8 @@ describe('Runner', () => {
               run: (value: [number]) => {
                 ++numCallsRun;
                 const successId = successIds.indexOf(value[0]);
-                if (successId !== -1) return successId === failAtId ? 'failed' : null;
+                if (successId !== -1)
+                  return successId === failAtId ? { error: new Error(), errorMessage: 'failed' } : null;
                 return new PreconditionFailure();
               },
             };
@@ -258,7 +259,7 @@ describe('Runner', () => {
             },
             shrink: () => Stream.nil(),
             run: (_value: [number]) => {
-              return ++numCallsRun < num ? null : 'error';
+              return ++numCallsRun < num ? null : { error: new Error(), errorMessage: 'error' };
             },
           };
           const out = check(p, { seed: seed }) as RunDetails<[number]>;
@@ -329,7 +330,7 @@ describe('Runner', () => {
           throw 'Not implemented';
         },
         run: (_value: [number]) => {
-          return 'failure';
+          return { error: new Error(), errorMessage: 'failure' };
         },
       };
       const out = check(p, { endOnFailure: true }) as RunDetails<[number]>;
@@ -349,7 +350,7 @@ describe('Runner', () => {
           return Stream.of(new Value([42], undefined));
         },
         run: (value: [number]) => {
-          return value[0] === 42 ? 'failure' : null;
+          return value[0] === 42 ? { error: new Error(), errorMessage: 'failure' } : null;
         },
       };
       const out = check(p, { path: '0:0', endOnFailure: true }) as RunDetails<[number]>;
@@ -361,7 +362,7 @@ describe('Runner', () => {
         isAsync: () => false,
         generate: () => new Value([42], undefined),
         shrink: () => Stream.nil(),
-        run: () => 'failure',
+        run: () => ({ error: new Error(), errorMessage: 'failure' }),
       };
       const out = check(p) as RunDetails<[number]>;
       expect(out.failures).toHaveLength(0);
@@ -375,7 +376,7 @@ describe('Runner', () => {
             ? Stream.of(new Value([48], undefined), new Value([12], undefined))
             : Stream.nil();
         },
-        run: () => 'failure',
+        run: () => ({ error: new Error(), errorMessage: 'failure' }),
       };
       const out = check(p, { verbose: true }) as RunDetails<[number]>;
       expect(out.failures).not.toHaveLength(0);
@@ -407,7 +408,7 @@ describe('Runner', () => {
             run: (_value: [number]) => {
               if (--remainingBeforeFailure >= 0) return null;
               remainingBeforeFailure = failurePoints[++idx];
-              return 'failure';
+              return { error: new Error(), errorMessage: 'failure' };
             },
           };
           const expectedFailurePath = failurePoints.join(':');
@@ -441,7 +442,7 @@ describe('Runner', () => {
               await new Promise<void>((resolve) => {
                 waitingResolve.push(resolve);
               });
-              return ++numCallsRun < num ? null : 'error';
+              return ++numCallsRun < num ? null : { error: new Error(), errorMessage: 'error' };
             },
           };
           const checker = check(p, { seed: seed }) as Promise<RunDetails<[number]>>;
@@ -519,13 +520,13 @@ describe('Runner', () => {
       isAsync: () => false,
       generate: () => new Value([v1, v2], undefined),
       shrink: () => Stream.nil(),
-      run: (_v: [any, any]) => 'error in failingProperty',
+      run: (_v: [any, any]) => ({ error: new Error(), errorMessage: 'error in failingProperty' }),
     };
     const failingComplexProperty: IRawProperty<[any, any, any]> = {
       isAsync: () => false,
       generate: () => new Value([[v1, v2], v2, v1], undefined),
       shrink: () => Stream.nil(),
-      run: (_v: [any, any, any]) => 'error in failingComplexProperty',
+      run: (_v: [any, any, any]) => ({ error: new Error(), errorMessage: 'error in failingComplexProperty' }),
     };
     const successProperty: IRawProperty<[any, any]> = {
       isAsync: () => false,
@@ -578,7 +579,7 @@ describe('Runner', () => {
             ? Stream.of(new Value([48], undefined), new Value([12], undefined))
             : Stream.nil();
         },
-        run: () => 'failure',
+        run: () => ({ error: new Error(), errorMessage: 'failure' }),
       };
       it('Should throw with base message by default (no verbose)', () => {
         expect(() => rAssert(p)).toThrowError(baseErrorMessage);
diff --git a/test/unit/check/runner/Tosser.spec.ts b/test/unit/check/runner/Tosser.spec.ts
index 808059b9..98cdd0f4 100644
--- a/test/unit/check/runner/Tosser.spec.ts
+++ b/test/unit/check/runner/Tosser.spec.ts
@@ -16,7 +16,7 @@ const wrap = <T>(arb: Arbitrary<T>): IRawProperty<T> =>
     isAsync = () => false;
     generate = (rng: Random) => new Value(this.arb.generate(rng, undefined).value_, undefined);
     shrink = () => Stream.nil<Value<T>>();
-    run = () => '';
+    run = () => ({ error: new Error(), errorMessage: 'failure' });
   })(arb);
 
 describe('Tosser', () => {