Skip to content

Commit 4874077

Browse files
nekomeowwwkwaa
andauthored
feat(std): error utils (#9)
* feat(std): new error related * fix: dirname * Update index.ts * Update index.ts * Update index.ts * Update index.ts * Update index.ts * Update index.ts * Update index.ts --------- Co-authored-by: 藍+85CD <50108258+kwaa@users.noreply.github.com>
1 parent 29a306a commit 4874077

4 files changed

Lines changed: 363 additions & 1 deletion

File tree

packages/std/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
".": "./src/index.ts",
1818
"./async-iterator": "./src/async-iterator/index.ts",
1919
"./base64": "./src/base64/index.ts",
20+
"./error": "./src/error/index.ts",
2021
"./merge": "./src/merge/index.ts",
2122
"./set-interval": "./src/set-interval/index.ts",
2223
"./sleep": "./src/sleep/index.ts",
@@ -40,6 +41,10 @@
4041
"types": "./dist/base64/index.d.ts",
4142
"default": "./dist/base64/index.js"
4243
},
44+
"./error": {
45+
"types": "./dist/error/index.d.ts",
46+
"default": "./dist/error/index.js"
47+
},
4348
"./merge": {
4449
"types": "./dist/merge/index.d.ts",
4550
"default": "./dist/merge/index.js"
@@ -67,6 +72,8 @@
6772
},
6873
"scripts": {
6974
"build": "pkgroll",
70-
"prebuild": "tsx scripts/update-exports.ts"
75+
"prebuild": "tsx scripts/update-exports.ts",
76+
"test": "vitest run",
77+
"test:watch": "vitest"
7178
}
7279
}

packages/std/src/error/index.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* ErrorLike utility interface for containing error-like objects.
3+
*/
4+
export type ErrorLike<C = unknown> = Nullable<Partial<Pick<Error, 'stack'>>> & Pick<Error, 'message' | 'name'> & { cause?: C }
5+
6+
type Nullable<T> = {
7+
[P in keyof T]: null | T[P]
8+
}
9+
10+
export const isErrorLike = <C = unknown>(err: null | undefined | unknown): err is ErrorLike<C> => {
11+
if (err == null)
12+
return false
13+
14+
if (err instanceof Error)
15+
return true
16+
17+
if (typeof err !== 'object')
18+
return false
19+
20+
const hasName = 'name' in err && typeof err.name === 'string'
21+
const hasMessage = 'message' in err && typeof err.message === 'string'
22+
return hasName && hasMessage
23+
}
24+
25+
export const errorNameFrom = (err: null | undefined | unknown): string | undefined =>
26+
isErrorLike(err)
27+
? err.name
28+
: undefined
29+
30+
export const errorMessageFrom = (err: null | undefined | unknown): string | undefined =>
31+
isErrorLike(err)
32+
? err.message
33+
: undefined
34+
35+
export const errorStackFrom = (err: null | undefined | unknown): null | string | undefined => {
36+
if (!isErrorLike(err)) {
37+
return undefined
38+
}
39+
if (err.stack == null) {
40+
const error = new Error(errorMessageFrom(err))
41+
return error.stack
42+
}
43+
44+
return err.stack
45+
}
46+
47+
export const errorCauseFrom = <C>(err: null | undefined | unknown): C | undefined => {
48+
if (!isErrorLike(err) || err.cause == null)
49+
return undefined
50+
51+
// eslint-disable-next-line @masknet/type-prefer-return-type-annotation
52+
return err.cause as C | undefined
53+
}
54+
55+
export const isErrorEq = (src: null | undefined | unknown, target: null | undefined | unknown): boolean => {
56+
if (!isErrorLike(src) || !isErrorLike(target)) {
57+
return false
58+
}
59+
60+
const srcName = errorNameFrom(src)
61+
const targetName = errorNameFrom(target)
62+
if (srcName == null || targetName == null) {
63+
return false
64+
}
65+
66+
if (srcName !== targetName) {
67+
return false
68+
}
69+
70+
const srcMessage = errorMessageFrom(src)
71+
const targetMessage = errorMessageFrom(target)
72+
if (srcMessage == null || targetMessage == null) {
73+
return false
74+
}
75+
76+
return srcMessage === targetMessage
77+
}
78+
79+
export const isErrorTypeEq = (src: null | undefined | unknown, target: null | undefined | unknown): boolean => {
80+
if (!isErrorLike(src) || !isErrorLike(target))
81+
return false
82+
83+
return errorNameFrom(src) === errorNameFrom(target)
84+
}

packages/std/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './async-iterator'
22
export * from './base64'
3+
export * from './error'
34
export * from './merge'
45
export * from './set-interval'
56
export * from './sleep'

packages/std/test/error.test.ts

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import {
4+
errorCauseFrom,
5+
errorMessageFrom,
6+
errorNameFrom,
7+
errorStackFrom,
8+
isError,
9+
isErrorEq,
10+
isErrorLike,
11+
isErrorTypeEq,
12+
} from '../src/error'
13+
14+
const nonObjectErrorLikeTestCases = [
15+
42,
16+
'Hello, world',
17+
Symbol('error'),
18+
true,
19+
false,
20+
BigInt(123),
21+
Number.NaN,
22+
]
23+
24+
const nonErrorLikeTestCases = [
25+
{},
26+
[],
27+
{ name: 'Error' },
28+
{ message: 'Error occurred' },
29+
{ message: 'Error occurred', name: 42 },
30+
{ message: 42, name: 'Error' },
31+
]
32+
33+
describe('isErrorLike', () => {
34+
it('should return true for Error', () => {
35+
const error = new Error('Test error')
36+
const result = isErrorLike(error)
37+
expect(result).toBe(true)
38+
})
39+
40+
it('should return true for manually created ErrorLike object', () => {
41+
const errorObj = {
42+
message: 'This is a custom error',
43+
name: 'CustomError',
44+
}
45+
46+
const result = isErrorLike(errorObj)
47+
expect(result).toBe(true)
48+
})
49+
50+
it('should return false for nullish values', () => {
51+
expect(isErrorLike(undefined)).toBe(false)
52+
expect(isErrorLike(null)).toBe(false)
53+
})
54+
55+
it('should return false for non-object values', () => {
56+
nonObjectErrorLikeTestCases.forEach((input) => {
57+
expect(isErrorLike(input)).toBe(false)
58+
})
59+
})
60+
61+
it('should return false for non ErrorLike values', () => {
62+
nonErrorLikeTestCases.forEach((input) => {
63+
expect(isErrorLike(input)).toBe(false)
64+
})
65+
})
66+
})
67+
68+
describe('isError', () => {
69+
it('should return true for Error', () => {
70+
const error = new Error('Test error')
71+
const result = isError(error)
72+
expect(result).toBe(true)
73+
})
74+
75+
it('should return false for manually created ErrorLike object', () => {
76+
const errorObj = {
77+
message: 'This is a custom error',
78+
name: 'CustomError',
79+
}
80+
81+
const result = isError(errorObj)
82+
expect(result).toBe(false)
83+
})
84+
85+
it('should return false for nullish values', () => {
86+
expect(isError(undefined)).toBe(false)
87+
expect(isError(null)).toBe(false)
88+
})
89+
90+
it('should return false for non-object values', () => {
91+
nonObjectErrorLikeTestCases.forEach((input) => {
92+
expect(isError(input)).toBe(false)
93+
})
94+
})
95+
96+
it('should return false for non ErrorLike values', () => {
97+
nonErrorLikeTestCases.forEach((input) => {
98+
expect(isError(input)).toBe(false)
99+
})
100+
})
101+
})
102+
103+
describe('errorNameFrom', () => {
104+
it('should return the name of an Error', () => {
105+
const name = errorNameFrom(new Error('Test error'))
106+
expect(name).toBe('Error')
107+
})
108+
109+
it('should return the name of a custom ErrorLike object', () => {
110+
const errorObj = {
111+
message: 'Custom error',
112+
name: 'CustomError',
113+
}
114+
115+
const result = errorNameFrom(errorObj)
116+
expect(result).toBe('CustomError')
117+
})
118+
119+
it('should return undefined for falsy isErrorLike values', () => {
120+
expect(errorNameFrom(undefined)).toBeUndefined()
121+
expect(errorNameFrom(null)).toBeUndefined()
122+
nonObjectErrorLikeTestCases.forEach((input) => {
123+
expect(errorNameFrom(input)).toBeUndefined()
124+
})
125+
nonErrorLikeTestCases.forEach((input) => {
126+
expect(errorNameFrom(input)).toBeUndefined()
127+
})
128+
})
129+
})
130+
131+
describe('errorMessageFrom', () => {
132+
it('should return the message of an Error', () => {
133+
const message = errorMessageFrom(new Error('Test error'))
134+
expect(message).toBe('Test error')
135+
})
136+
137+
it('should return the message of a custom ErrorLike object', () => {
138+
const errorObj = {
139+
message: 'Custom error',
140+
name: 'CustomError',
141+
}
142+
143+
const result = errorMessageFrom(errorObj)
144+
expect(result).toBe('Custom error')
145+
})
146+
147+
it('should return undefined for falsy isErrorLike values', () => {
148+
expect(errorMessageFrom(undefined)).toBeUndefined()
149+
expect(errorMessageFrom(null)).toBeUndefined()
150+
nonObjectErrorLikeTestCases.forEach((input) => {
151+
expect(errorMessageFrom(input)).toBeUndefined()
152+
})
153+
nonErrorLikeTestCases.forEach((input) => {
154+
expect(errorMessageFrom(input)).toBeUndefined()
155+
})
156+
})
157+
})
158+
159+
describe('errorStackFrom', () => {
160+
it('should return the stack of an Error', () => {
161+
const error = new Error('Test error')
162+
const stack = errorStackFrom(error)
163+
expect(stack).toBeDefined()
164+
expect(typeof stack).toBe('string')
165+
})
166+
167+
it('should return a new Error stack if the original does not have one', () => {
168+
const errorObj = {
169+
message: 'Custom error without stack',
170+
name: 'CustomError',
171+
}
172+
const stack = errorStackFrom(errorObj)
173+
expect(stack).toBeDefined()
174+
expect(typeof stack).toBe('string')
175+
})
176+
177+
it('should return undefined for falsy isErrorLike values', () => {
178+
expect(errorStackFrom(undefined)).toBeUndefined()
179+
expect(errorStackFrom(null)).toBeUndefined()
180+
nonObjectErrorLikeTestCases.forEach((input) => {
181+
expect(errorStackFrom(input)).toBeUndefined()
182+
})
183+
nonErrorLikeTestCases.forEach((input) => {
184+
expect(errorStackFrom(input)).toBeUndefined()
185+
})
186+
})
187+
})
188+
189+
describe('errorCauseFrom', () => {
190+
it('should return the cause of an Error', () => {
191+
const cause = new Error('Cause error')
192+
const error = new Error('Test error', { cause })
193+
const result = errorCauseFrom(error)
194+
expect(result).toBe(cause)
195+
})
196+
197+
it('should return undefined if the cause is not present', () => {
198+
const errorObj = {
199+
message: 'Custom error without cause',
200+
name: 'CustomError',
201+
}
202+
const result = errorCauseFrom(errorObj)
203+
expect(result).toBeUndefined()
204+
})
205+
206+
it('should return undefined for falsy isErrorLike values', () => {
207+
expect(errorCauseFrom(undefined)).toBeUndefined()
208+
expect(errorCauseFrom(null)).toBeUndefined()
209+
nonObjectErrorLikeTestCases.forEach((input) => {
210+
expect(errorCauseFrom(input)).toBeUndefined()
211+
})
212+
nonErrorLikeTestCases.forEach((input) => {
213+
expect(errorCauseFrom(input)).toBeUndefined()
214+
})
215+
})
216+
})
217+
218+
describe('isErrorEq', () => {
219+
it('should return true for equal Error objects', () => {
220+
const error1 = new Error('Test error')
221+
const error2 = new Error('Test error')
222+
expect(isErrorEq(error1, error2)).toBe(true)
223+
})
224+
225+
it('should return false for different Error objects', () => {
226+
const error1 = new Error('Test error 1')
227+
const error2 = new Error('Test error 2')
228+
expect(isErrorEq(error1, error2)).toBe(false)
229+
})
230+
231+
it('should return false for non-ErrorLike values', () => {
232+
expect(isErrorEq(undefined, null)).toBe(false)
233+
nonObjectErrorLikeTestCases.forEach((input) => {
234+
expect(isErrorEq(input, new Error('Error occurred'))).toBe(false)
235+
expect(isErrorEq(new Error('Error occurred'), input)).toBe(false)
236+
})
237+
nonErrorLikeTestCases.forEach((input) => {
238+
expect(isErrorEq(input, new Error('Error occurred'))).toBe(false)
239+
expect(isErrorEq(new Error('Error occurred'), input)).toBe(false)
240+
})
241+
})
242+
})
243+
244+
describe('isErrorTypeEq', () => {
245+
it('should return true for equal ErrorLike types', () => {
246+
const error1 = new Error('Test error')
247+
const error2 = new Error('Test error')
248+
expect(isErrorTypeEq(error1, error2)).toBe(true)
249+
})
250+
251+
it('should return false for different ErrorLike types', () => {
252+
const error1 = new Error('Test error 1')
253+
error1.name = 'CustomError1'
254+
const error2 = new Error('Test error 2')
255+
error2.name = 'CustomError2'
256+
expect(isErrorTypeEq(error1, error2)).toBe(false)
257+
})
258+
259+
it('should return false for non-ErrorLike values', () => {
260+
expect(isErrorTypeEq(undefined, null)).toBe(false)
261+
nonObjectErrorLikeTestCases.forEach((input) => {
262+
expect(isErrorTypeEq(input, new Error('Error occurred'))).toBe(false)
263+
expect(isErrorTypeEq(new Error('Error occurred'), input)).toBe(false)
264+
})
265+
nonErrorLikeTestCases.forEach((input) => {
266+
expect(isErrorTypeEq(input, new Error('Error occurred'))).toBe(false)
267+
expect(isErrorTypeEq(new Error('Error occurred'), input)).toBe(false)
268+
})
269+
})
270+
})

0 commit comments

Comments
 (0)