Skip to content

Commit 13e0466

Browse files
committed
Refactor FirebaseError
1 parent b6bfae0 commit 13e0466

File tree

3 files changed

+84
-125
lines changed

3 files changed

+84
-125
lines changed

packages/util/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export {
3030
ErrorFactory,
3131
ErrorList,
3232
FirebaseError,
33-
patchCapture,
3433
StringLike
3534
} from './src/errors';
3635
export { jsonEval, stringify } from './src/json';

packages/util/src/errors.ts

Lines changed: 58 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -54,115 +54,95 @@
5454
* }
5555
* }
5656
*/
57-
export type ErrorList<T> = { [code: string]: string };
57+
export type ErrorList<T extends string = string> = {
58+
readonly [K in T]: string
59+
};
5860

5961
const ERROR_NAME = 'FirebaseError';
6062

6163
export interface StringLike {
6264
toString: () => string;
6365
}
6466

65-
let captureStackTrace: (obj: Object, fn?: Function) => void = (Error as any)
66-
.captureStackTrace;
67-
68-
// Export for faking in tests
69-
export function patchCapture(captureFake?: any): any {
70-
let result: any = captureStackTrace;
71-
captureStackTrace = captureFake;
72-
return result;
67+
export interface ErrorData {
68+
[key: string]: StringLike | undefined;
7369
}
7470

7571
export interface FirebaseError {
7672
// Unique code for error - format is service/error-code-string
77-
code: string;
73+
readonly code: string;
7874

7975
// Developer-friendly error message.
80-
message: string;
76+
readonly message: string;
8177

8278
// Always 'FirebaseError'
83-
name: string;
79+
readonly name: typeof ERROR_NAME;
8480

8581
// Where available - stack backtrace in a string
86-
stack: string;
87-
}
82+
readonly stack: string;
8883

89-
export class FirebaseError implements FirebaseError {
90-
public stack: string;
91-
public name: string;
92-
93-
constructor(public code: string, public message: string) {
94-
let stack: string;
95-
// We want the stack value, if implemented by Error
96-
if (captureStackTrace) {
97-
// Patches this.stack, omitted calls above ErrorFactory#create
98-
captureStackTrace(this, ErrorFactory.prototype.create);
99-
} else {
100-
try {
101-
// In case of IE11, stack will be set only after error is raised.
102-
// https://docs.microsoft.com/en-us/scripting/javascript/reference/stack-property-error-javascript
103-
throw Error.apply(this, arguments);
104-
} catch (err) {
105-
this.name = ERROR_NAME;
106-
// Make non-enumerable getter for the property.
107-
Object.defineProperty(this, 'stack', {
108-
get: function() {
109-
return err.stack;
110-
}
111-
});
112-
}
113-
}
114-
}
84+
// Additional custom error data that was used in the template.
85+
readonly data: ErrorData;
11586
}
11687

117-
// Back-door inheritance
118-
FirebaseError.prototype = Object.create(Error.prototype) as FirebaseError;
119-
FirebaseError.prototype.constructor = FirebaseError;
120-
(FirebaseError.prototype as any).name = ERROR_NAME;
121-
122-
export class ErrorFactory<T extends string> {
123-
// Matches {$name}, by default.
124-
public pattern = /\{\$([^}]+)}/g;
88+
// Based on code from:
89+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types
90+
export class FirebaseError extends Error {
91+
readonly name = ERROR_NAME;
12592

12693
constructor(
127-
private service: string,
128-
private serviceName: string,
129-
private errors: ErrorList<T>
94+
readonly code: string,
95+
message: string,
96+
readonly data: ErrorData = {}
13097
) {
131-
// empty
132-
}
98+
super(message);
99+
100+
// Fix For ES5
101+
// https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
102+
Object.setPrototypeOf(this, FirebaseError.prototype);
133103

134-
create(code: T, data?: { [prop: string]: StringLike }): FirebaseError {
135-
if (data === undefined) {
136-
data = {};
104+
// Maintains proper stack trace for where our error was thrown.
105+
// Only available on V8.
106+
if (Error.captureStackTrace) {
107+
Error.captureStackTrace(this, ErrorFactory.prototype.create);
137108
}
109+
}
110+
}
138111

139-
let template = this.errors[code as string];
112+
export class ErrorFactory<ErrorCode extends string> {
113+
constructor(
114+
private readonly service: string,
115+
private readonly serviceName: string,
116+
private readonly errors: ErrorList<ErrorCode>
117+
) {}
140118

141-
let fullCode = this.service + '/' + code;
142-
let message: string;
119+
create(code: ErrorCode, data: ErrorData = {}): FirebaseError {
120+
const fullCode = `${this.service}/${code}`;
121+
const template = this.errors[code];
143122

144-
if (template === undefined) {
145-
message = 'Error';
146-
} else {
147-
message = template.replace(this.pattern, (match, key) => {
148-
let value = data![key];
149-
return value !== undefined ? value.toString() : '<' + key + '?>';
150-
});
151-
}
123+
const message = template ? replaceTemplate(template, data) : 'Error';
152124

153125
// Service: Error message (service/code).
154-
message = this.serviceName + ': ' + message + ' (' + fullCode + ').';
155-
let err = new FirebaseError(fullCode, message);
156-
157-
// Populate the Error object with message parts for programmatic
158-
// accesses (e.g., e.file).
159-
for (let prop in data) {
160-
if (!data.hasOwnProperty(prop) || prop.slice(-1) === '_') {
161-
continue;
126+
const fullMessage = `${this.serviceName}: ${message} (${fullCode}).`;
127+
128+
// Keys with an underscore at the end of their name are not included in
129+
// error.data for some reason.
130+
const filteredData: ErrorData = {};
131+
// TODO: Replace with Object.entries when lib is updated to es2017.
132+
for (const key of Object.keys(data)) {
133+
if (key.slice(-1) !== '_') {
134+
filteredData[key] = data[key];
162135
}
163-
(err as any)[prop] = data[prop];
164136
}
165-
166-
return err;
137+
return new FirebaseError(fullCode, fullMessage, filteredData);
167138
}
168139
}
140+
141+
function replaceTemplate(template: string, data: ErrorData): string {
142+
return template.replace(PATTERN, (_, key) => {
143+
let value = data != null ? data[key] : undefined;
144+
return value != null ? value.toString() : `<${key}?>`;
145+
});
146+
}
147+
148+
const PATTERN = /\{\$([^}]+)}/g;

packages/util/test/errors.test.ts

Lines changed: 26 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -15,96 +15,76 @@
1515
* limitations under the License.
1616
*/
1717
import { assert } from 'chai';
18-
import { ErrorFactory, ErrorList, patchCapture } from '../src/errors';
18+
import { ErrorFactory, ErrorList, FirebaseError } from '../src/errors';
1919

2020
type Err = 'generic-error' | 'file-not-found' | 'anon-replace';
2121

22-
let errors = {
22+
let errors: ErrorList<Err> = {
2323
'generic-error': 'Unknown error',
2424
'file-not-found': "Could not find file: '{$file}'",
2525
'anon-replace': 'Hello, {$repl_}!'
26-
} as ErrorList<Err>;
26+
};
2727

2828
let error = new ErrorFactory<Err>('fake', 'Fake', errors);
2929

3030
describe('FirebaseError', () => {
31-
it('create', () => {
31+
it('creates an Error', () => {
3232
let e = error.create('generic-error');
33+
assert.instanceOf(e, Error);
34+
assert.instanceOf(e, FirebaseError);
3335
assert.equal(e.code, 'fake/generic-error');
3436
assert.equal(e.message, 'Fake: Unknown error (fake/generic-error).');
3537
});
3638

37-
it('String replacement', () => {
39+
it('replaces template values with data', () => {
3840
let e = error.create('file-not-found', { file: 'foo.txt' });
3941
assert.equal(e.code, 'fake/file-not-found');
4042
assert.equal(
4143
e.message,
4244
"Fake: Could not find file: 'foo.txt' (fake/file-not-found)."
4345
);
44-
assert.equal((e as any).file, 'foo.txt');
46+
assert.equal(e.data.file, 'foo.txt');
4547
});
4648

47-
it('Anonymous String replacement', () => {
49+
it('anonymously replaces template values with data', () => {
4850
let e = error.create('anon-replace', { repl_: 'world' });
4951
assert.equal(e.code, 'fake/anon-replace');
5052
assert.equal(e.message, 'Fake: Hello, world! (fake/anon-replace).');
51-
assert.isUndefined((e as any).repl_);
53+
assert.isUndefined(e.data.repl_);
5254
});
5355

54-
it('Missing template', () => {
56+
it('uses "Error" as template when template is missing', () => {
5557
// Cast to avoid compile-time error.
5658
let e = error.create(('no-such-code' as any) as Err);
5759
assert.equal(e.code, 'fake/no-such-code');
5860
assert.equal(e.message, 'Fake: Error (fake/no-such-code).');
5961
});
6062

61-
it('Missing replacement', () => {
63+
it('uses the key in the template if the replacement is missing', () => {
6264
let e = error.create('file-not-found', { fileX: 'foo.txt' });
6365
assert.equal(e.code, 'fake/file-not-found');
6466
assert.equal(
6567
e.message,
6668
"Fake: Could not find file: '<file?>' (fake/file-not-found)."
6769
);
6870
});
69-
});
70-
71-
// Run the stack trace tests with, and without, Error.captureStackTrace
72-
let realCapture = patchCapture();
73-
stackTests(realCapture);
74-
stackTests(undefined);
75-
76-
function stackTests(fakeCapture: any) {
77-
let saveCapture: any;
78-
79-
describe(
80-
'Error#stack tests - Error.captureStackTrace is ' +
81-
(fakeCapture ? 'defined' : 'NOT defined'),
82-
() => {
83-
before(() => {
84-
saveCapture = patchCapture(fakeCapture);
85-
});
86-
87-
after(() => {
88-
patchCapture(saveCapture);
89-
});
9071

91-
it('has stack', () => {
92-
let e = error.create('generic-error');
93-
// Multi-line match trick - .* does not match \n
94-
assert.match(e.stack, /FirebaseError[\s\S]/);
95-
});
72+
it('has stack', () => {
73+
let e = error.create('generic-error');
74+
// Multi-line match trick - .* does not match \n
75+
assert.match(e.stack, /FirebaseError[\s\S]/);
76+
});
9677

97-
it('stack frames', () => {
98-
try {
99-
dummy1();
100-
assert.ok(false);
101-
} catch (e) {
102-
assert.match(e.stack, /dummy2[\s\S]*?dummy1/);
103-
}
104-
});
78+
it('has function names in stack trace in correct order', () => {
79+
try {
80+
dummy1();
81+
assert.ok(false);
82+
} catch (e) {
83+
assert.instanceOf(e, FirebaseError);
84+
assert.match(e.stack, /dummy2[\s\S]*?dummy1/);
10585
}
106-
);
107-
}
86+
});
87+
});
10888

10989
function dummy1() {
11090
dummy2();

0 commit comments

Comments
 (0)