|
| 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 | +``` |
0 commit comments