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" ;
613import type { AsyncFn , Fn , NonEmptyTuple , SyncFn } from "@/types" ;
714
15+ const nil = null as never ;
16+
817export 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
233266export 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
251284export 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
269386export const ok = Result . ok ;
0 commit comments