Skip to content

Commit 8df9b3b

Browse files
committed
feat(3.0.0-rc.5): implement tagged error type in err.ts
This commit primarily implements the Err type in err.ts. It is intended to standardize error types such that errors from different computations can be type unioned instead of deconstructed into application errors. The secondary tag allows for some intelligent mapping at the end of the pipeline, ideally useful for perhaps turning different tagged errors into http status codes, etc. Originally I had implemented Combinable for Err as a Free combinable, but I found that this would complicate the structure of errors. Instead I am opting to allow the user to either wrap the Err type in their own structure or to embed errors in the typed context field. If I find that this was the wrong direction I hope to fix it before the 3.0.0 release.
1 parent 04db8a3 commit 8df9b3b

6 files changed

Lines changed: 337 additions & 140 deletions

File tree

deno.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@baetheus/fun",
3-
"version": "3.0.0-rc.4",
3+
"version": "3.0.0-rc.5",
44
"exports": {
55
"./applicable": "./applicable.ts",
66
"./array": "./array.ts",
@@ -17,6 +17,7 @@
1717
"./decoder": "./decoder.ts",
1818
"./effect": "./effect.ts",
1919
"./either": "./either.ts",
20+
"./err": "./err.ts",
2021
"./failable": "./failable.ts",
2122
"./filterable": "./filterable.ts",
2223
"./flatmappable": "./flatmappable.ts",

err.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* This file contains the Err algebraic data type and related utilities for
3+
* creating and matching typed errors with optional context.
4+
*
5+
* @module Err
6+
* @since 3.0.0-rc.5
7+
*/
8+
9+
/**
10+
* The Err type represents a typed error with a name, message, and optional context.
11+
*
12+
* @example
13+
* ```ts
14+
* import { err } from "./err.ts";
15+
*
16+
* const ValidationError = err("ValidationError");
17+
* const error = ValidationError("Invalid input", { field: "email" });
18+
*
19+
* // error is of type Err<"ValidationError", { field: string }>
20+
* console.log(error.name); // "ValidationError"
21+
* console.log(error.message); // "Invalid input"
22+
* console.log(error.context); // { field: "email" }
23+
* ```
24+
*
25+
* @since 3.0.0-rc.5
26+
*/
27+
export type Err<T extends string, A> = {
28+
readonly tag: "Error";
29+
readonly name: T;
30+
readonly message: string;
31+
readonly context?: A;
32+
};
33+
34+
/**
35+
* A type alias for any Err type, useful for type constraints.
36+
*
37+
* @example
38+
* ```ts
39+
* import type { AnyErr } from "./err.ts";
40+
*
41+
* function handleError(error: AnyErr) {
42+
* console.log(error.name, error.message);
43+
* }
44+
* ```
45+
*
46+
* @since 3.0.0-rc.5
47+
*/
48+
// deno-lint-ignore no-explicit-any
49+
export type AnyErr = Err<string, any>;
50+
51+
/**
52+
* Create a constructor function for a specific error type.
53+
*
54+
* @example
55+
* ```ts
56+
* import { err } from "./err.ts";
57+
*
58+
* const NotFoundError = err("NotFoundError");
59+
* const error = NotFoundError("Resource not found", { id: 123 });
60+
*
61+
* // error.name === "NotFoundError"
62+
* // error.message === "Resource not found"
63+
* // error.context === { id: 123 }
64+
* ```
65+
*
66+
* @since 3.0.0-rc.5
67+
*/
68+
export function err<T extends string>(
69+
name: T,
70+
): <A>(message: string, context?: A) => Err<T, A> {
71+
return (message, context) => ({
72+
tag: "Error",
73+
name,
74+
message,
75+
context,
76+
});
77+
}
78+
79+
// deno-lint-ignore no-explicit-any
80+
type ExtractTags<T> = T extends Err<infer Tag, any> ? Tag : never;
81+
82+
type MatchTag<Tag, Errors> = Tag extends string
83+
? Errors extends Err<Tag, infer A> ? Err<Tag, A> : never
84+
: never;
85+
86+
type MapFunc<T, B> = T extends Err<string, infer A>
87+
? (message: string, context: A) => B
88+
: never;
89+
90+
type ToRecord<T, B> = { [K in ExtractTags<T>]: MapFunc<MatchTag<K, T>, B> };
91+
92+
/**
93+
* Pattern match on an error value, dispatching to the appropriate handler
94+
* based on the error's name.
95+
*
96+
* @since 3.0.0-rc.5
97+
*/
98+
export function match<T extends AnyErr, B>(
99+
fns: ToRecord<T, B>,
100+
): (ta: T) => B {
101+
return (ta) => (fns[ta.name as keyof ToRecord<T, B>])(ta.message, ta.context);
102+
}
103+
104+

examples/tagged_error.ts

Lines changed: 0 additions & 106 deletions
This file was deleted.

ideas/callbag.ts

Lines changed: 26 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/**
2+
* A talkback is the method for backpressure in our interpretation of the
3+
* callbag specification. Specifically, we
4+
*/
15
export type Talkback = (count: number) => void;
26

37
export type Talk<A> = {
@@ -23,8 +27,6 @@ export type TypeOf<S> = S extends Stream<infer A, infer _> ? A : never;
2327

2428
export type EnvOf<S> = S extends Stream<infer _, infer E> ? E : never;
2529

26-
export const Disposed = Symbol("fun/stream/disposed");
27-
2830
export function disposable(dispose: (reason?: unknown) => void): Disposable {
2931
return { [Symbol.dispose]: dispose };
3032
}
@@ -33,26 +35,14 @@ export function disposable(dispose: (reason?: unknown) => void): Disposable {
3335
* @todo: Consider how "safe" a sink needs to work.
3436
*/
3537
export function sink<A>(snk: Sink<A>): Sink<A> {
36-
return snk;
37-
// return function safeSink(tlkbk) {
38-
// let open = true;
39-
// const talk = snk(tlkbk);
40-
// return {
41-
// event: (value) => {
42-
// if (open) {
43-
// return talk.event(value);
44-
// }
45-
// throw new Error("Talk event called after close.");
46-
// },
47-
// end: (reason) => {
48-
// if (open) {
49-
// open = false;
50-
// return talk.end(reason);
51-
// }
52-
// throw new Error("Talk end called after close.");
53-
// },
54-
// };
55-
// };
38+
return function safeSink(tlkbk) {
39+
let open = true;
40+
const talk = snk(tlkbk);
41+
return {
42+
event: (value) => open && talk.event(value),
43+
end: (reason) => open && !(open = false) && talk.end(reason),
44+
};
45+
};
5646
}
5747

5848
export function stream<A, R = unknown>(strm: Stream<A, R>): Stream<A, R> {
@@ -245,17 +235,20 @@ export function loop<A, B, S>(
245235
): <E>(ua: Stream<A, E>) => Stream<B, E> {
246236
return (ua) => (snk, env) => {
247237
let state = seed;
248-
return ua((tlkbk) => {
249-
const talk = snk(tlkbk);
250-
return {
251-
event: (a) => {
252-
const [next, b] = stepper(state, a);
253-
state = next;
254-
talk.event(b);
255-
},
256-
end: talk.end,
257-
};
258-
}, env);
238+
return ua(
239+
sink((tlkbk) => {
240+
const talk = snk(tlkbk);
241+
return {
242+
event: (a) => {
243+
const [next, b] = stepper(state, a);
244+
state = next;
245+
talk.event(b);
246+
},
247+
end: talk.end,
248+
};
249+
}),
250+
env,
251+
);
259252
};
260253
}
261254

ideas/codec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* I think it's time to extend Decoder to be a full codec. The basic idea is
3+
* that a Codec can decode from I to A and encode from A to O. Most of the time
4+
* I will be unknown and O will be the same as A. But there is room for cases
5+
* such as Option<A> encoding to and decoding from A or null or undefined. This
6+
* will allow for cleaner mapping to and from internal representations. In
7+
* tandem with this I would like to update Schemable to allow for some of the
8+
* more useful filters on strings, numbers, record domain as well as range, and
9+
* anything else that might be useful in general.
10+
*/
11+
import type * as Either from "../either.ts";
12+
13+
import * as Err from "../err.ts";
14+
15+
export const leaf = Err.err("DecodeErrorLeaf");
16+
17+
export type Codec<I, A, O> = {
18+
decode: (input: I) => Either.Either<Err.AnyErr, A>;
19+
encode: (value: A) => O;
20+
};

0 commit comments

Comments
 (0)