Repository: tinylibs/tinybench (ts)

Issue:
Is there a reason that bench.run() doesn't throw?
I was scratching my head for a good three hours until I did this:

```js
bench.addEventListener("error", e => reportError(e))
```

Scenario:

```js
const bench = new Bench()
bench.add("i look fast but i just fail", () => { iDontExist() })
await bench.run()
console.table(bench.table())
```

Is there a reason that right now throwing inside benchmarks doesn't cause a catastrophic failure? There must  be... right? 😅

I think that it would be a good idea to make it so that either:
1. bench.run() throws
2. console.error() on any errors

Some kind of user feedback that the superfast speeds they're seeing are actually _an error_ is a good idea. 😊

Pull request:
feat: `throws`
Resolves #47 


Patch:
diff --git a/README.md b/README.md
index 414d8c2..ea20294 100644
--- a/README.md
+++ b/README.md
@@ -110,6 +110,11 @@ export type Options = {
    */
   signal?: AbortSignal;
 
+  /**
+   * Throw if a task fails (events will not work if true)
+   */
+  throws?: boolean;
+
   /**
    * warmup time (milliseconds) @default 100ms
    */
diff --git a/package.json b/package.json
index 23fdf91..3b3d794 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
     "publish": "npm run build && clean-publish",
     "typecheck": "tsc --noEmit",
     "release": "bumpp package.json --commit --push --tag && npm run publish",
-    "test": "vitest --run"
+    "test": "vitest --retry=3 --run"
   },
   "main": "./dist/index.cjs",
   "module": "./dist/index.js",
diff --git a/src/bench.ts b/src/bench.ts
index b46bf18..676b8ec 100644
--- a/src/bench.ts
+++ b/src/bench.ts
@@ -6,7 +6,7 @@ import type {
   TaskResult,
   BenchEventsMap,
   FnOptions,
-} from '../types/index';
+} from './types';
 import { createBenchEvent } from './event';
 import Task from './task';
 import { AddEventListenerOptionsArgument, RemoveEventListenerOptionsArgument } from './types';
@@ -26,6 +26,8 @@ export default class Bench extends EventTarget {
 
   signal?: AbortSignal;
 
+  throws: boolean;
+
   warmupTime = 100;
 
   warmupIterations = 5;
@@ -48,6 +50,7 @@ export default class Bench extends EventTarget {
     this.time = options.time ?? this.time;
     this.iterations = options.iterations ?? this.iterations;
     this.signal = options.signal;
+    this.throws = options.throws ?? false;
     // eslint-disable-next-line @typescript-eslint/no-empty-function
     this.setup = options.setup ?? (() => {});
     // eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -158,10 +161,10 @@ export default class Bench extends EventTarget {
       if (result) {
         return {
           'Task Name': name,
-          'ops/sec': parseInt(result.hz.toString(), 10).toLocaleString(),
-          'Average Time (ns)': result.mean * 1000 * 1000,
-          Margin: `\xb1${result.rme.toFixed(2)}%`,
-          Samples: result.samples.length,
+          'ops/sec': result.error ? 'NaN' : parseInt(result.hz.toString(), 10).toLocaleString(),
+          'Average Time (ns)': result.error ? 'NaN' : result.mean * 1000 * 1000,
+          Margin: result.error ? 'NaN' : `\xb1${result.rme.toFixed(2)}%`,
+          Samples: result.error ? 'NaN' : result.samples.length,
         };
       }
       return null;
diff --git a/src/event.ts b/src/event.ts
index 49d3c05..ff9066c 100644
--- a/src/event.ts
+++ b/src/event.ts
@@ -1,4 +1,4 @@
-import type { BenchEvents } from '../types';
+import type { BenchEvents } from './types';
 import Task from './task';
 
 function createBenchEvent(
diff --git a/src/index.ts b/src/index.ts
index ac75634..5b2fb2d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -9,7 +9,7 @@ export type {
   Hook,
   TaskEvents,
   BenchEvent,
-} from '../types';
+} from './types';
 
 export { now, hrtimeNow } from './utils';
 
diff --git a/src/task.ts b/src/task.ts
index 444d4cd..da9d162 100644
--- a/src/task.ts
+++ b/src/task.ts
@@ -4,7 +4,7 @@ import type {
   TaskResult,
   TaskEventsMap,
   FnOptions,
-} from '../types/index';
+} from './types';
 import Bench from './bench';
 import tTable from './constants';
 import { createBenchEvent } from './event';
@@ -100,6 +100,9 @@ export default class Task extends EventTarget {
       }
     } catch (e) {
       this.setResult({ error: e });
+      if (this.bench.throws) {
+        throw e;
+      }
     }
 
     if (this.opts.afterAll != null) {
@@ -217,8 +220,10 @@ export default class Task extends EventTarget {
         } else {
           this.fn.call(this);
         }
-      } catch {
-        // todo
+      } catch (e) {
+        if (this.bench.throws) {
+          throw e;
+        }
       }
 
       this.runs += 1;
diff --git a/src/types.ts b/src/types.ts
index b965c7a..2e0bb75 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,3 +1,243 @@
+import Task from '../src/task';
+
+/**
+ * the task function
+ */
+export type Fn = () => any | Promise<any>;
+
+export interface FnOptions {
+  /**
+   * An optional function that is run before iterations of this task begin
+   */
+  beforeAll?: (this: Task) => void | Promise<void>;
+
+  /**
+   * An optional function that is run before each iteration of this task
+   */
+  beforeEach?: (this: Task) => void | Promise<void>;
+
+  /**
+   * An optional function that is run after each iteration of this task
+   */
+  afterEach?: (this: Task) => void | Promise<void>;
+
+  /**
+   * An optional function that is run after all iterations of this task end
+   */
+  afterAll?: (this: Task) => void | Promise<void>;
+}
+
+/**
+ * the benchmark task result object
+ */
+export type TaskResult = {
+  /*
+   * the last error that was thrown while running the task
+   */
+  error?: unknown;
+
+  /**
+   * The amount of time in milliseconds to run the benchmark task (cycle).
+   */
+  totalTime: number;
+
+  /**
+   * the minimum value in the samples
+   */
+  min: number;
+  /**
+   * the maximum value in the samples
+   */
+  max: number;
+
+  /**
+   * the number of operations per second
+   */
+  hz: number;
+
+  /**
+   * how long each operation takes (ms)
+   */
+  period: number;
+
+  /**
+   * task samples of each task iteration time (ms)
+   */
+  samples: number[];
+
+  /**
+   * samples mean/average (estimate of the population mean)
+   */
+  mean: number;
+
+  /**
+   * samples variance (estimate of the population variance)
+   */
+  variance: number;
+
+  /**
+   * samples standard deviation (estimate of the population standard deviation)
+   */
+  sd: number;
+
+  /**
+   * standard error of the mean (a.k.a. the standard deviation of the sampling distribution of the sample mean)
+   */
+  sem: number;
+
+  /**
+   * degrees of freedom
+   */
+  df: number;
+
+  /**
+   * critical value of the samples
+   */
+  critical: number;
+
+  /**
+   * margin of error
+   */
+  moe: number;
+
+  /**
+   * relative margin of error
+   */
+  rme: number;
+
+  /**
+   * p75 percentile
+   */
+  p75: number;
+
+  /**
+   * p99 percentile
+   */
+  p99: number;
+
+  /**
+   * p995 percentile
+   */
+  p995: number;
+
+  /**
+   * p999 percentile
+   */
+  p999: number;
+};
+
+/**
+  * Both the `Task` and `Bench` objects extend the `EventTarget` object,
+  * so you can attach a listeners to different types of events
+  * to each class instance using the universal `addEventListener` and
+ * `removeEventListener`
+ */
+
+/**
+ * Bench events
+ */
+export type BenchEvents =
+  | 'abort' // when a signal aborts
+  | 'complete' // when running a benchmark finishes
+  | 'error' // when the benchmark task throws
+  | 'reset' // when the reset function gets called
+  | 'start' // when running the benchmarks gets started
+  | 'warmup' // when the benchmarks start getting warmed up (before start)
+  | 'cycle' // when running each benchmark task gets done (cycle)
+  | 'add' // when a Task gets added to the Bench
+  | 'remove' // when a Task gets removed of the Bench
+  | 'todo'; // when a todo Task gets added to the Bench
+
+export type Hook = (task: Task, mode: 'warmup' | 'run') => void | Promise<void>;
+
+type NoopEventListener = () => any | Promise<any>
+type TaskEventListener = (e: Event & { task: Task }) => any | Promise<any>
+
+export interface BenchEventsMap{
+  abort: NoopEventListener
+  start: NoopEventListener
+  complete: NoopEventListener
+  warmup: NoopEventListener
+  reset: NoopEventListener
+  add: TaskEventListener
+  remove: TaskEventListener
+  cycle: TaskEventListener
+  error: TaskEventListener
+  todo: TaskEventListener
+}
+
+/**
+ * task events
+ */
+export type TaskEvents =
+  | 'abort'
+  | 'complete'
+  | 'error'
+  | 'reset'
+  | 'start'
+  | 'warmup'
+  | 'cycle';
+
+export type TaskEventsMap = {
+  abort: NoopEventListener
+  start: TaskEventListener
+  error: TaskEventListener
+  cycle: TaskEventListener
+  complete: TaskEventListener
+  warmup: TaskEventListener
+  reset: TaskEventListener
+}
+export type Options = {
+  /**
+   * time needed for running a benchmark task (milliseconds) @default 500
+   */
+  time?: number;
+
+  /**
+   * number of times that a task should run if even the time option is finished @default 10
+   */
+  iterations?: number;
+
+  /**
+   * function to get the current timestamp in milliseconds
+   */
+  now?: () => number;
+
+  /**
+   * An AbortSignal for aborting the benchmark
+   */
+  signal?: AbortSignal;
+
+  /**
+   * Throw if a task fails (events will not work if true)
+   */
+  throws?: boolean;
+
+  /**
+   * warmup time (milliseconds) @default 100ms
+   */
+  warmupTime?: number;
+
+  /**
+   * warmup iterations @default 5
+   */
+  warmupIterations?: number;
+
+  /**
+   * setup function to run before each benchmark task (cycle)
+   */
+  setup?: Hook;
+
+  /**
+   * teardown function to run after each benchmark task (cycle)
+   */
+  teardown?: Hook;
+};
+
+export type BenchEvent = Event & {
+  task: Task | null;
+};
+
 // @types/node doesn't have these types globally, and we don't want to bring "dom" lib for everyone
 
 export type RemoveEventListenerOptionsArgument = Parameters<typeof EventTarget.prototype.removeEventListener>[2];
diff --git a/src/utils.ts b/src/utils.ts
index 8f54fd6..bc69a96 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,4 +1,4 @@
-import type { Fn } from '../types/index';
+import type { Fn } from './types';
 import type Task from './task';
 
 export const nanoToMs = (nano: number) => nano / 1e6;
diff --git a/types/index.d.ts b/types/index.d.ts
deleted file mode 100644
index 1257766..0000000
--- a/types/index.d.ts
+++ /dev/null
@@ -1,234 +0,0 @@
-import Task from '../src/task';
-
-/**
- * the task function
- */
-export type Fn = () => any | Promise<any>;
-
-export interface FnOptions {
-  /**
-   * An optional function that is run before iterations of this task begin
-   */
-  beforeAll?: (this: Task) => void | Promise<void>;
-
-  /**
-   * An optional function that is run before each iteration of this task
-   */
-  beforeEach?: (this: Task) => void | Promise<void>;
-
-  /**
-   * An optional function that is run after each iteration of this task
-   */
-  afterEach?: (this: Task) => void | Promise<void>;
-
-  /**
-   * An optional function that is run after all iterations of this task end
-   */
-  afterAll?: (this: Task) => void | Promise<void>;
-}
-
-/**
- * the benchmark task result object
- */
-export type TaskResult = {
-  /*
-   * the last error that was thrown while running the task
-   */
-  error?: unknown;
-
-  /**
-   * The amount of time in milliseconds to run the benchmark task (cycle).
-   */
-  totalTime: number;
-
-  /**
-   * the minimum value in the samples
-   */
-  min: number;
-  /**
-   * the maximum value in the samples
-   */
-  max: number;
-
-  /**
-   * the number of operations per second
-   */
-  hz: number;
-
-  /**
-   * how long each operation takes (ms)
-   */
-  period: number;
-
-  /**
-   * task samples of each task iteration time (ms)
-   */
-  samples: number[];
-
-  /**
-   * samples mean/average (estimate of the population mean)
-   */
-  mean: number;
-
-  /**
-   * samples variance (estimate of the population variance)
-   */
-  variance: number;
-
-  /**
-   * samples standard deviation (estimate of the population standard deviation)
-   */
-  sd: number;
-
-  /**
-   * standard error of the mean (a.k.a. the standard deviation of the sampling distribution of the sample mean)
-   */
-  sem: number;
-
-  /**
-   * degrees of freedom
-   */
-  df: number;
-
-  /**
-   * critical value of the samples
-   */
-  critical: number;
-
-  /**
-   * margin of error
-   */
-  moe: number;
-
-  /**
-   * relative margin of error
-   */
-  rme: number;
-
-  /**
-   * p75 percentile
-   */
-  p75: number;
-
-  /**
-   * p99 percentile
-   */
-  p99: number;
-
-  /**
-   * p995 percentile
-   */
-  p995: number;
-
-  /**
-   * p999 percentile
-   */
-  p999: number;
-};
-
-/**
-  * Both the `Task` and `Bench` objects extend the `EventTarget` object,
-  * so you can attach a listeners to different types of events
-  * to each class instance using the universal `addEventListener` and
- * `removeEventListener`
- */
-
-/**
- * Bench events
- */
-export type BenchEvents =
-  | 'abort' // when a signal aborts
-  | 'complete' // when running a benchmark finishes
-  | 'error' // when the benchmark task throws
-  | 'reset' // when the reset function gets called
-  | 'start' // when running the benchmarks gets started
-  | 'warmup' // when the benchmarks start getting warmed up (before start)
-  | 'cycle' // when running each benchmark task gets done (cycle)
-  | 'add' // when a Task gets added to the Bench
-  | 'remove' // when a Task gets removed of the Bench
-  | 'todo'; // when a todo Task gets added to the Bench
-
-export type Hook = (task: Task, mode: 'warmup' | 'run') => void | Promise<void>;
-
-type NoopEventListener = () => any | Promise<any>
-type TaskEventListener = (e: Event & { task: Task }) => any | Promise<any>
-
-export interface BenchEventsMap{
-  abort: NoopEventListener
-  start: NoopEventListener
-  complete: NoopEventListener
-  warmup: NoopEventListener
-  reset: NoopEventListener
-  add: TaskEventListener
-  remove: TaskEventListener
-  cycle: TaskEventListener
-  error: TaskEventListener
-  todo: TaskEventListener
-}
-
-/**
- * task events
- */
-export type TaskEvents =
-  | 'abort'
-  | 'complete'
-  | 'error'
-  | 'reset'
-  | 'start'
-  | 'warmup'
-  | 'cycle';
-
-export type TaskEventsMap = {
-  abort: NoopEventListener
-  start: TaskEventListener
-  error: TaskEventListener
-  cycle: TaskEventListener
-  complete: TaskEventListener
-  warmup: TaskEventListener
-  reset: TaskEventListener
-}
-export type Options = {
-  /**
-   * time needed for running a benchmark task (milliseconds) @default 500
-   */
-  time?: number;
-
-  /**
-   * number of times that a task should run if even the time option is finished @default 10
-   */
-  iterations?: number;
-
-  /**
-   * function to get the current timestamp in milliseconds
-   */
-  now?: () => number;
-
-  /**
-   * An AbortSignal for aborting the benchmark
-   */
-  signal?: AbortSignal;
-
-  /**
-   * warmup time (milliseconds) @default 100ms
-   */
-  warmupTime?: number;
-
-  /**
-   * warmup iterations @default 5
-   */
-  warmupIterations?: number;
-
-  /**
-   * setup function to run before each benchmark task (cycle)
-   */
-  setup?: Hook;
-
-  /**
-   * teardown function to run after each benchmark task (cycle)
-   */
-  teardown?: Hook;
-};
-
-export type BenchEvent = Event & {
-  task: Task | null;
-};


Test patch:
diff --git a/test/index.test.ts b/test/index.test.ts
index 6b279c3..5b42b26 100644
--- a/test/index.test.ts
+++ b/test/index.test.ts
@@ -221,6 +221,16 @@ test('error event', async () => {
   expect(taskErr!).toBe(err);
 });
 
+test('throws', async () => {
+  const bench = new Bench({ iterations: 1, throws: true });
+  const err = new Error();
+
+  bench.add('error', () => {
+    throw err;
+  });
+  expect(() => bench.run()).rejects.toThrowError(err);
+});
+
 test('detect faster task', async () => {
   const bench = new Bench({ time: 200 });
   bench