Skip to content

Commit de7dda6

Browse files
committed
feat: enhance Err result with context and stack trace options
1 parent 922920c commit de7dda6

File tree

2 files changed

+156
-10
lines changed

2 files changed

+156
-10
lines changed

src/result/result.ts

Lines changed: 127 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
/* eslint-disable @typescript-eslint/unified-signatures */
22

3-
import { isFunction, isPromiseLike } from "@/remeda";
4-
5-
import type { InferErrType, InferOkType, ResultAll, ResultAllSettled } from "./types";
3+
import { isFunction, isObjectType, isPromiseLike, isString } from "@/remeda";
4+
5+
import type {
6+
InferErrType,
7+
InferOkType,
8+
PrintOptions,
9+
PrintPresets,
10+
ResultAll,
11+
ResultAllSettled,
12+
} from "./types";
613
import type { AsyncFn, Fn, NonEmptyTuple, SyncFn } from "@/types";
714

15+
const nil = null as never;
16+
817
export abstract class Result<T = unknown, E = unknown> {
918
static ok(): Ok<void>;
1019
static ok<T>(value: T): Ok<T>;
@@ -15,7 +24,17 @@ export abstract class Result<T = unknown, E = unknown> {
1524
static err(): Err<void>;
1625
static err<E>(error: E): Err<E>;
1726
static err(error?: unknown): Err {
18-
return new Err(error);
27+
const err = new Err(error);
28+
29+
if (error instanceof Error) {
30+
err["stack"] = error.stack;
31+
} else if ("captureStackTrace" in Error) {
32+
const dummy = {} as unknown as Error;
33+
Error.captureStackTrace(dummy, Result.err);
34+
err["stack"] = dummy.stack;
35+
}
36+
37+
return err;
1938
}
2039

2140
static try<T, E = unknown>(fn: SyncFn<T>): Result<T, E>;
@@ -86,6 +105,8 @@ export abstract class Result<T = unknown, E = unknown> {
86105
return acc;
87106
}
88107

108+
protected readonly ctxs: (string | Fn<string>)[] = [];
109+
89110
abstract readonly ok: boolean;
90111

91112
/**
@@ -207,9 +228,9 @@ export abstract class Result<T = unknown, E = unknown> {
207228
*/
208229
iter(): [ok: boolean, error: E, value: T] {
209230
if (this.isOk()) {
210-
return [true, null as never, this.value];
231+
return [true, nil, this.value];
211232
} else {
212-
return [false, this.error, null as never];
233+
return [false, this.error, nil];
213234
}
214235
}
215236

@@ -225,14 +246,26 @@ export abstract class Result<T = unknown, E = unknown> {
225246
return self as unknown as T;
226247
}
227248

249+
context(context: string): this {
250+
this.ctxs.push(context);
251+
252+
return this;
253+
}
254+
255+
withContext(fn: Fn<string>): this {
256+
this.ctxs.push(fn);
257+
258+
return this;
259+
}
260+
228261
abstract get value(): T;
229262

230263
abstract get error(): E;
231264
}
232265

233266
export class Ok<T = unknown> extends Result<T, never> {
234267
readonly ok = true;
235-
private _value: T;
268+
private readonly _value: T;
236269

237270
constructor(value: T) {
238271
super();
@@ -244,26 +277,110 @@ export class Ok<T = unknown> extends Result<T, never> {
244277
}
245278

246279
get error(): never {
247-
return null as never;
280+
return nil;
248281
}
249282
}
250283

251284
export class Err<E = unknown> extends Result<never, E> {
252285
readonly ok = false;
253-
private _error: E;
286+
private readonly _error: E;
287+
private stack: string | undefined;
254288

255289
constructor(error: E) {
256290
super();
257291
this._error = error;
258292
}
259293

260294
get value(): never {
261-
return null as never;
295+
return nil;
262296
}
263297

264298
get error(): E {
265299
return this._error;
266300
}
301+
302+
print(): void;
303+
print(preset: PrintPresets): void;
304+
print(options: PrintOptions): void;
305+
print(presetOrOptions?: PrintPresets | PrintOptions): void {
306+
const options: Required<PrintOptions> = {
307+
level: "error",
308+
context: true,
309+
stack: false,
310+
};
311+
if (isString(presetOrOptions)) {
312+
options.context = presetOrOptions === "full" || presetOrOptions === "standard";
313+
options.stack = presetOrOptions === "full";
314+
} else if (isObjectType(presetOrOptions)) {
315+
options.level = presetOrOptions.level ?? options.level;
316+
options.context = presetOrOptions.context ?? options.context;
317+
options.stack = presetOrOptions.stack ?? options.stack;
318+
}
319+
320+
const output = this.format(options.context, options.stack);
321+
322+
switch (options.level) {
323+
case "error":
324+
console.error(output);
325+
break;
326+
case "warn":
327+
console.warn(output);
328+
break;
329+
case "info":
330+
console.info(output);
331+
break;
332+
}
333+
}
334+
335+
private format(context: boolean, stack: boolean): string {
336+
const contexts = this.ctxs
337+
.slice()
338+
.toReversed()
339+
.map(ctx => (isFunction(ctx) ? ctx() : ctx));
340+
const stacks = this.stack
341+
?.split("\n")
342+
.map(line => line.trim())
343+
.filter(Boolean) || ["<no stack trace>"];
344+
345+
let message: string;
346+
try {
347+
message =
348+
this._error instanceof Error ? this._error.message : JSON.stringify(this._error);
349+
} catch {
350+
message = String(this._error);
351+
}
352+
353+
const lines: (string | string[])[] = [
354+
`Error: ${contexts.length > 0 ? contexts.at(0) : message}`,
355+
];
356+
357+
if (context) {
358+
lines.push(
359+
"",
360+
"Caused by:",
361+
contexts
362+
.slice(1)
363+
.concat(message)
364+
.map((line, index) => ` ${index}: ${line}`),
365+
);
366+
}
367+
368+
if (stack) {
369+
const top = stacks.at(0) || "";
370+
const hasErrorMessage =
371+
new RegExp(`^\\w+:\\s+${message}$`).test(top) || /^\w+$/.test(top);
372+
373+
lines.push(
374+
"",
375+
"Stack trace:",
376+
stacks.slice(hasErrorMessage ? 1 : 0).map(line => ` ${line}`),
377+
);
378+
}
379+
380+
const output = lines.flat().join("\n");
381+
382+
return output;
383+
}
267384
}
268385

269386
export const ok = Result.ok;

src/result/types.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
import type { Err, Ok, Result } from "./result";
22

3+
/**
4+
* Presets for printing the `Err` result
5+
*
6+
* - "full" - Prints full details including context and stack trace.
7+
* - "standard" - Prints error message and context, but omits stack trace.
8+
* - "minimal" - Prints only the error message without context or stack trace.
9+
*
10+
* Default is "standard".
11+
*/
12+
export type PrintPresets = "full" | "standard" | "minimal";
13+
14+
/**
15+
* Options for printing the `Err` result
16+
*/
17+
export interface PrintOptions {
18+
/**
19+
* The log level to use. Default is "error".
20+
*/
21+
level?: "error" | "warn" | "info";
22+
/**
23+
* Whether to include the context messages. Default is `true`.
24+
*/
25+
context?: boolean;
26+
/**
27+
* Whether to include the stack trace. Default is `false`.
28+
*/
29+
stack?: boolean;
30+
}
31+
332
export type ExtractOkTypes<T extends readonly Result[]> = {
433
[K in keyof T]: T[K] extends Result<infer U, unknown> ? U : never;
534
};

0 commit comments

Comments
 (0)