Skip to content

Commit dcccaab

Browse files
committed
Squash changes for a clean slate
1 parent cb518dc commit dcccaab

18 files changed

Lines changed: 4303 additions & 0 deletions

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
dist
3+
.idea
4+
.DS_Store
5+
.npmrc
6+
.envrc

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 SMBCheeky
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
## TL;DR
2+
3+
- Install the package `npm install @smbcheeky/error-object`
4+
- Write `ErrorObject.from(<pick an api response with an error>).force?.verboseLog('LOG')`
5+
- :tada:
6+
- oh... and check the [playground](https://github.com/SMBCheeky/error-object/blob/main/playground/index.ts) file
7+
8+
## Opening words from SMBCheeky
9+
10+
The ErrorObject class was created to help developers deal with errors in a consistent and predictable way. Along the
11+
years, and across tens of projects, it has gathered quite a few features, and it was time to share it with the world.
12+
13+
I've been maintaining some version of this library and/or its principles on every project I worked on in the past 10
14+
years, across multiple languages and frameworks. But it has always been a copy-paste-modify-test-again cycle, and I am
15+
tired :) I took a few days to write this package... then re-write it... then refactor it... then delete 80%... then
16+
repeat everything 2 more times.
17+
18+
You may not like what I've shared here, but I think it's a good conversation starter.
19+
20+
I hope you find this library useful, and I'm looking forward to hearing your feedback.
21+
22+
## Installation
23+
24+
`npm install @smbcheeky/error-object`
25+
26+
`yarn add @smbcheeky/error-object`
27+
28+
## Compatibility
29+
30+
It should work with any modern Javascript or Typescript project. Let me know if I can do anything to improve the
31+
compatibility even more. PRs are welcome.
32+
33+
## *Short* description
34+
35+
Pick a backend endpoint error response, a 3rd party library error response, or even an error and write this one line of
36+
code:
37+
38+
```typescript
39+
ErrorObject.from(response).force?.verboseLog('LOG')
40+
```
41+
42+
-> you will have a smile on your face, but you may not understand what just happened.
43+
44+
Let's break it down:
45+
46+
- The ErrorObject class is made to extend `Error` enabling checks like `errorObject instanceof Error` or
47+
`errorObject instanceof ErrorObject`.
48+
- It can be thrown or returned, you choose.
49+
- `new ErrorObject()` still works as an Error object, but it has a few more features.
50+
- It can be valid only if it contains a `code` and a `message` values
51+
- It can have a numberCode, not just a string code
52+
- set default values for the generic and fallback error objects via `ErrorObject.DEFAULT_GENERIC_CODE` and
53+
`ErrorObject.DEFAULT_GENERIC_MESSAGE`
54+
- set a default domain for all errors via `ErrorObject.DEFAULT_DOMAIN`
55+
- Use `ErrorObject.generic()` or `ErrorObject.withTag('TAG')` to create an error from thin air
56+
- Use `.isGeneric()`, `.isFallback()` and `.hasTag()` to check if the error is a generic error, a fallback error or
57+
has a specific tag
58+
- Chain call setters like `.setCode()`, `.setNumberCode()`, `.setMessage()`, `.setDetails()`, `.setDomain()`,
59+
`.setTag()` to modify the error
60+
object at any moment
61+
- Setters can receive a value or a transform function, facilitating access to the current value while you modify the
62+
property
63+
- Chain logs like `.log(tag)`, `.debugLog(tag)`, `.verboseLog(tag)` to log information about the error object
64+
inline
65+
- Use `.description()` or `.toString()` to get a human-readable description of the error
66+
- Use `details`, `domain` and `tag` to customize the error object and help easily distinguish between different
67+
errors
68+
- It can be used to create errors from anything, using `ErrorObject.from(<anything>)`, which will return an
69+
`ErrorObject` instance, but there are a few things to know before starting:
70+
- you can pass an object or a caught error to it, and it will try its best to create an error from it
71+
- `ErrorObject.from(<anything>)` returns an object with two properties: `.error` and `.force`
72+
- `.error` represents the error, if it can be created, otherwise it is `undefined`
73+
- `.force` represents the error, if it can be created, otherwise it is going to return a `ErrorObject.fallback()`
74+
error
75+
- the processing of the ErrorObject is done in a few steps, based on the `ErrorObjectBuildOptions`:
76+
- first the initial object is checked via the options `checkInputObjectForValues` and `checkInputObjectForTypes`
77+
and `checkInputObjectForKeys`
78+
- then the objects checks for an object array at `pathToErrors`, which could be an array of errors
79+
- if an error array is found, the process will consider all other paths relative to the objects in the error
80+
array found
81+
- if an error array is not found, the process will consider all other paths absolute to the initial object
82+
passed to `ErrorObject.from()`
83+
- the `pathToCode`, `pathToNumberCode`, `pathToMessage`, `pathToDetails` and `pathToDomain` options are used to
84+
map values to their associated field, if found
85+
- for all fields other than `numberCode`, if a value is found and is a string, it is saved as is, but if it is
86+
an array or an object it will be JSON.stringify'ed and saved as a string
87+
- for `numberCode`, if a value is found and it is a number different than `NaN`, it is saved
88+
- the `transformCode`, `transformNumberCode`, `transformMessage`, `transformDetails` and `transformDomain`
89+
functions are used to transform the found values to the error object
90+
- the transform functions have access to each respective value, all other values, and the initial object (object
91+
inside the errors array or initial object)
92+
- everything gets processed into a list of `ErrorSummary | ErrorObjectErrorResult` array
93+
- it contains everything, from error strings custom-made to be as distinct and easy to read as possible, to self
94+
documenting summaries of what values are found, at which path, if an errors object was found, etc.
95+
- the count of the list is meant to be an indication of how many input objects were found and processed, as each
96+
of them should become an error object
97+
- in the last step of the process, the list is filtered down and a single error object is created, with
98+
everything baked in
99+
- think detailed `processingErrors` which includes the summaries and the errors that were triggered during
100+
the process, the `raw` object that was used as in input for the ErrorObject.from() call and the `nextErrors`
101+
array which allows for all errors to be saved on one single error object for later use
102+
103+
Now, there are a few more features that I may have missed, but I think you get the idea - It does a lot of work
104+
so that you can focus on the task at hand - keeping the user informed on what to do when things go wrong.
105+
106+
## Core concepts
107+
108+
- The code should be simple to understand and easily patchable or extended to suit any needs
109+
- "Provide the best defaults possible, but don't block the developer from customizing it"
110+
- "I shall not use generics" - If the result is 50x more readable, yeah sure, maybe...
111+
- "Name everything like there is no documentation" - Do not open PRs with refactored code for anything until you
112+
understand this phrase
113+
- "Do not practice black magic" - You can hide things for a bit, make them magic... but don't make any developer's
114+
life harder, they still have to deal with timezones on a self-hosted mysql database, that has "TODAY" as a valid
115+
date... don't ask, just don't...
116+
- "Write code that is easily debuggable so fixes are easy as well" - Just common sense, if you ask me
117+
- "The code you write is to help the end user, not you, your boss or your client" - Maybe once you see this, you will
118+
tell your users what "... went wrong" :)
119+
120+
## Usage & Examples
121+
122+
For a guide on how to use the library, please check the first detailed example in
123+
the [playground](https://github.com/SMBCheeky/error-object/blob/main/playground/index.ts) file.
124+
125+
Some simple examples:
126+
127+
```typescript
128+
new ErrorObject({ code: '', message: 'Something went wrong.', domain: 'auth' }).debugLog('LOG');
129+
130+
ErrorObject.from({ code: '', message: 'Something went wrong', domain: 'auth' })?.force?.debugLog('LOG');
131+
132+
// Example 12 output:
133+
//
134+
// [LOG] Something went wrong. [auth]
135+
// {
136+
// "code": "",
137+
// "message": "Something went wrong.",
138+
// "domain": "auth"
139+
// }
140+
//
141+
// [LOG] Something went wrong [auth]
142+
// {
143+
// "code": "",
144+
// "message": "Something went wrong",
145+
// "domain": "auth"
146+
// }
147+
```
148+
149+
```typescript
150+
const response = {
151+
statusCode: 400,
152+
headers: {
153+
'Content-Type': 'application/json',
154+
},
155+
body: '{"error":"Invalid input data","code":400}',
156+
};
157+
158+
ErrorObject.from(JSON.parse(response?.body), {
159+
pathToNumberCode: ['code'],
160+
pathToMessage: ['error'],
161+
}).force?.debugLog('LOG');
162+
163+
// Example 6 output:
164+
//
165+
// [LOG] Invalid input data [400]
166+
// {
167+
// "code": "400",
168+
// "numberCode": 400,
169+
// "message": "Invalid input data"
170+
// }
171+
```
172+
173+
```typescript
174+
/*
175+
* You could have a file called `errors.ts` in each of your modules/folders and
176+
* define a function like `createAuthError2()` that returns an error object with
177+
* the correct message and domain.
178+
*/
179+
const AuthMessageResolver = (
180+
message: string | undefined,
181+
beforeTransform: ErrorObjectBeforeTransformState): string => {
182+
// Quick tip: Make all messages slightly different, to make it easy
183+
// to find the right one when debugging, even in production
184+
switch (beforeTransform.code) {
185+
case 'generic':
186+
return 'Something went wrong';
187+
case 'generic-again':
188+
return 'Something went wrong. Please try again.';
189+
case 'generic-network':
190+
return 'Something went wrong. Please check your internet connection and try again.';
191+
default:
192+
return 'Something went wrong.';
193+
}
194+
};
195+
196+
const createAuthError2 = (code: string) => {
197+
return ErrorObject.from(
198+
{
199+
code,
200+
domain: 'auth',
201+
},
202+
{
203+
transformMessage: AuthMessageResolver,
204+
},
205+
);
206+
};
207+
208+
209+
createAuthError2('generic')?.error?.log('1');
210+
createAuthError2('generic-again')?.error?.log('2');
211+
createAuthError2('generic-network')?.error?.log('3');
212+
createAuthError2('invalid-code')?.error?.log('4');
213+
214+
// Example 2 output:
215+
//
216+
// [1] Something went wrong [auth/generic]
217+
// [2] Something went wrong. Please try again. [auth/generic-again]
218+
// [3] Something went wrong. Please check your internet connection and try again. [auth/generic-network]
219+
// [4] Something went wrong. [auth/invalid-code]
220+
```

package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "@smbcheeky/error-object",
3+
"version": "1.0.0",
4+
"main": "dist/index.js",
5+
"module": "dist/index.mjs",
6+
"types": "dist/index.d.ts",
7+
"scripts": {
8+
"build": "tsup src/index.ts --format cjs,esm --dts",
9+
"release": "yarn build && direnv exec . npm publish --access public",
10+
"lint": "tsc --noEmit"
11+
},
12+
"devDependencies": {
13+
"tsup": "^6.5.0",
14+
"typescript": "^4.9.4"
15+
},
16+
"license": "MIT",
17+
"private": false
18+
}

playground/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
dist
3+
.idea
4+
.DS_Store
5+
.npmrc
6+
.envrc

playground/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
I use this folder to test the library locally, before publishing it to npm.
2+
3+
If you want to do the same,
4+
5+
- `cd playground`
6+
- `npm install`
7+
- `npm run build`
8+
9+
and everything should be ready to go after that + hot reloading credits to `nodemon`.

0 commit comments

Comments
 (0)