Skip to content
This repository was archived by the owner on Jun 10, 2024. It is now read-only.

Commit c6c01d7

Browse files
committed
feat!: Throw custom errors instead of generics.
Changes errors into specific types so it's easier to determine what type of error has occurred.
1 parent 172be96 commit c6c01d7

File tree

2 files changed

+127
-10
lines changed

2 files changed

+127
-10
lines changed

‎src/object-schema.js‎

Lines changed: 107 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,108 @@ function validateDefinition(name, strategy) {
6262
}
6363
}
6464

65+
//-----------------------------------------------------------------------------
66+
// Errors
67+
//-----------------------------------------------------------------------------
68+
69+
/**
70+
* Error when an unexpected key is found.
71+
*/
72+
class UnexpectedKeyError extends Error {
73+
74+
/**
75+
* Creates a new instance.
76+
* @param {string} key The key that was unexpected.
77+
*/
78+
constructor(key) {
79+
super(`Unexpected key "${key}" found.`);
80+
}
81+
}
82+
83+
/**
84+
* Error when a required key is missing.
85+
*/
86+
class MissingKeyError extends Error {
87+
88+
/**
89+
* Creates a new instance.
90+
* @param {string} key The key that was missing.
91+
*/
92+
constructor(key) {
93+
super(`Missing required key "${key}".`);
94+
}
95+
}
96+
97+
/**
98+
* Error when a key requires other keys that are missing.
99+
*/
100+
class MissingDependentKeysError extends Error {
101+
102+
/**
103+
* Creates a new instance.
104+
* @param {string} key The key that was unexpected.
105+
* @param {Array<string>} requiredKeys The keys that are required.
106+
*/
107+
constructor(key, requiredKeys) {
108+
super(`Key "${key}" requires keys "${requiredKeys.join("\", \"")}".`);
109+
}
110+
}
111+
112+
/**
113+
* Wrapper error for errors occuring during a merge or validate operation.
114+
*/
115+
class WrapperError extends Error {
116+
117+
/**
118+
* Creates a new instance.
119+
* @param {string} key The object key causing the error.
120+
* @param {Error} source The source error.
121+
*/
122+
constructor(key, source) {
123+
super(`Key "${key}": ` + source.message);
124+
125+
/**
126+
* The original source error.
127+
* @type {Error}
128+
*/
129+
this.source = source;
130+
}
131+
132+
/**
133+
* The stack from the original error.
134+
* @returns {string}
135+
*/
136+
get stack() {
137+
return this.source.stack;
138+
}
139+
140+
/**
141+
* The line number from the original error.
142+
* @returns {number}
143+
*/
144+
get lineNumber() {
145+
return this.source.lineNumber;
146+
}
147+
148+
/**
149+
* The column number from the original error.
150+
* @returns {number}
151+
*/
152+
get columnNumber() {
153+
return this.source.columnNumber;
154+
}
155+
156+
/**
157+
* The filename from the original error.
158+
* @returns {string}
159+
*/
160+
get fileName() {
161+
return this.source.fileName;
162+
}
163+
}
65164

66165
//-----------------------------------------------------------------------------
67-
// Class
166+
// Main
68167
//-----------------------------------------------------------------------------
69168

70169
/**
@@ -159,11 +258,11 @@ class ObjectSchema {
159258

160259
// double check arguments
161260
if (objects.length < 2) {
162-
throw new Error("merge() requires at least two arguments.");
261+
throw new TypeError("merge() requires at least two arguments.");
163262
}
164263

165264
if (objects.some(object => (object == null || typeof object !== "object"))) {
166-
throw new Error("All arguments must be objects.");
265+
throw new TypeError("All arguments must be objects.");
167266
}
168267

169268
return objects.reduce((result, object) => {
@@ -179,8 +278,7 @@ class ObjectSchema {
179278
}
180279
}
181280
} catch (ex) {
182-
ex.message = `Key "${key}": ` + ex.message;
183-
throw ex;
281+
throw new WrapperError(key, ex);
184282
}
185283
}
186284
return result;
@@ -200,7 +298,7 @@ class ObjectSchema {
200298

201299
// check to see if the key is defined
202300
if (!this.hasKey(key)) {
203-
throw new Error(`Unexpected key "${key}" found.`);
301+
throw new UnexpectedKeyError(key);
204302
}
205303

206304
// validate existing keys
@@ -209,23 +307,22 @@ class ObjectSchema {
209307
// first check to see if any other keys are required
210308
if (Array.isArray(strategy.requires)) {
211309
if (!strategy.requires.every(otherKey => otherKey in object)) {
212-
throw new Error(`Key "${key}" requires keys "${strategy.requires.join("\", \"")}".`);
310+
throw new MissingDependentKeysError(key, strategy.requires);
213311
}
214312
}
215313

216314
// now apply remaining validation strategy
217315
try {
218316
strategy.validate.call(strategy, object[key]);
219317
} catch (ex) {
220-
ex.message = `Key "${key}": ` + ex.message;
221-
throw ex;
318+
throw new WrapperError(key, ex);
222319
}
223320
}
224321

225322
// ensure required keys aren't missing
226323
for (const [key] of this[requiredKeys]) {
227324
if (!(key in object)) {
228-
throw new Error(`Missing required key "${key}".`);
325+
throw new MissingKeyError(key);
229326
}
230327
}
231328

‎tests/object-schema.js‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,26 @@ describe("ObjectSchema", () => {
110110

111111
});
112112

113+
it("should throw an error when merge() throws an error with a readonly message", () => {
114+
let schema = new ObjectSchema({
115+
foo: {
116+
merge() {
117+
throw {
118+
get message() {
119+
return "Boom!";
120+
}
121+
};
122+
},
123+
validate() {}
124+
}
125+
});
126+
127+
assert.throws(() => {
128+
schema.merge({ foo: true }, { foo: true });
129+
}, /Key "foo": Boom!/);
130+
131+
});
132+
113133
it("should call the merge() strategy for one key when called", () => {
114134

115135
schema = new ObjectSchema({

0 commit comments

Comments
 (0)