Skip to content

Commit 12e5a82

Browse files
committed
Refactor FirebaseError
1 parent b758647 commit 12e5a82

File tree

6 files changed

+129
-162
lines changed

6 files changed

+129
-162
lines changed

packages/app-types/private.d.ts

+2-14
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
import { FirebaseApp, FirebaseNamespace } from '@firebase/app-types';
2424
import { Observer, Subscribe } from '@firebase/util';
25-
import { FirebaseError } from '@firebase/util';
25+
import { FirebaseError, ErrorFactory } from '@firebase/util';
2626

2727
export interface FirebaseServiceInternals {
2828
/**
@@ -61,18 +61,6 @@ export interface FirebaseServiceNamespace<T extends FirebaseService> {
6161
(app?: FirebaseApp): T;
6262
}
6363

64-
export interface FirebaseErrorFactory<T> {
65-
create(code: T, data?: { [prop: string]: any }): FirebaseError;
66-
}
67-
68-
export interface FirebaseErrorFactoryClass {
69-
new (
70-
service: string,
71-
serviceName: string,
72-
errors: { [code: string]: string }
73-
): FirebaseErrorFactory<any>;
74-
}
75-
7664
export interface FirebaseAuthTokenData {
7765
accessToken: string;
7866
}
@@ -155,6 +143,6 @@ export interface _FirebaseNamespace extends FirebaseNamespace {
155143
/**
156144
* Use to construct all thrown FirebaseError's.
157145
*/
158-
ErrorFactory: FirebaseErrorFactoryClass;
146+
ErrorFactory: typeof ErrorFactory;
159147
};
160148
}

packages/app/src/errors.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { ErrorFactory } from '@firebase/util';
18+
import { ErrorFactory, ErrorMap } from '@firebase/util';
1919

2020
export const enum AppError {
2121
NO_APP = 'no-app',
@@ -26,7 +26,7 @@ export const enum AppError {
2626
INVALID_APP_ARGUMENT = 'invalid-app-argument'
2727
}
2828

29-
const errors: { readonly [code in AppError]: string } = {
29+
const errors: ErrorMap<AppError> = {
3030
[AppError.NO_APP]:
3131
"No Firebase App '{$name}' has been created - " +
3232
'call Firebase App.initializeApp()',
@@ -40,7 +40,7 @@ const errors: { readonly [code in AppError]: string } = {
4040
'Firebase App instance.'
4141
};
4242

43-
let appErrors = new ErrorFactory<AppError>('app', 'Firebase', errors);
43+
const appErrors = new ErrorFactory('app', 'Firebase', errors);
4444

4545
export function error(code: AppError, args?: { [name: string]: any }) {
4646
throw appErrors.create(code, args);

packages/messaging/src/models/errors.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { ErrorFactory } from '@firebase/util';
18+
import { ErrorFactory, ErrorMap } from '@firebase/util';
1919

2020
export const enum ErrorCode {
2121
AVAILABLE_IN_WINDOW = 'only-available-in-window',
@@ -59,7 +59,7 @@ export const enum ErrorCode {
5959
PUBLIC_KEY_DECRYPTION_FAILED = 'public-vapid-key-decryption-failed'
6060
}
6161

62-
export const ERROR_MAP: { [code in ErrorCode]: string } = {
62+
export const ERROR_MAP: ErrorMap<ErrorCode> = {
6363
[ErrorCode.AVAILABLE_IN_WINDOW]:
6464
'This method is available in a Window context.',
6565
[ErrorCode.AVAILABLE_IN_SW]:
@@ -156,7 +156,7 @@ export const ERROR_MAP: { [code in ErrorCode]: string } = {
156156
'The public VAPID key did not equal ' + '65 bytes when decrypted.'
157157
};
158158

159-
export const errorFactory: ErrorFactory<string> = new ErrorFactory(
159+
export const errorFactory = new ErrorFactory(
160160
'messaging',
161161
'Messaging',
162162
ERROR_MAP

packages/util/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ export {
2828
} from './src/environment';
2929
export {
3030
ErrorFactory,
31-
ErrorList,
31+
ErrorMap,
3232
FirebaseError,
33-
patchCapture,
3433
StringLike
3534
} from './src/errors';
3635
export { jsonEval, stringify } from './src/json';

packages/util/src/errors.ts

+64-85
Original file line numberDiff line numberDiff line change
@@ -54,115 +54,94 @@
5454
* }
5555
* }
5656
*/
57-
export type ErrorList<T> = { [code: string]: string };
57+
58+
export type ErrorMap<ErrorCode extends string> = {
59+
readonly [K in ErrorCode]: string
60+
};
5861

5962
const ERROR_NAME = 'FirebaseError';
6063

6164
export interface StringLike {
62-
toString: () => string;
65+
toString(): string;
6366
}
6467

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;
68+
export interface ErrorData {
69+
[key: string]: StringLike | undefined;
7370
}
7471

75-
export interface FirebaseError {
76-
// Unique code for error - format is service/error-code-string
77-
code: string;
72+
export interface FirebaseError extends Error, ErrorData {
73+
// Unique code for error - format is service/error-code-string.
74+
readonly code: string;
7875

7976
// Developer-friendly error message.
80-
message: string;
77+
readonly message: string;
8178

82-
// Always 'FirebaseError'
83-
name: string;
79+
// Always 'FirebaseError'.
80+
readonly name: typeof ERROR_NAME;
8481

85-
// Where available - stack backtrace in a string
86-
stack: string;
82+
// Where available - stack backtrace in a string.
83+
readonly stack?: string;
8784
}
8885

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-
}
115-
}
116-
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;
86+
// Based on code from:
87+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types
88+
export class FirebaseError extends Error {
89+
readonly name = ERROR_NAME;
12190

122-
export class ErrorFactory<T extends string> {
123-
// Matches {$name}, by default.
124-
public pattern = /\{\$([^}]+)}/g;
91+
constructor(readonly code: string, message: string) {
92+
super(message);
12593

126-
constructor(
127-
private service: string,
128-
private serviceName: string,
129-
private errors: ErrorList<T>
130-
) {
131-
// empty
132-
}
94+
// Fix For ES5
95+
// https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
96+
Object.setPrototypeOf(this, FirebaseError.prototype);
13397

134-
create(code: T, data?: { [prop: string]: StringLike }): FirebaseError {
135-
if (data === undefined) {
136-
data = {};
98+
// Maintains proper stack trace for where our error was thrown.
99+
// Only available on V8.
100+
if (Error.captureStackTrace) {
101+
Error.captureStackTrace(this, ErrorFactory.prototype.create);
137102
}
103+
}
104+
}
138105

139-
let template = this.errors[code as string];
106+
export class ErrorFactory<ErrorCode extends string> {
107+
constructor(
108+
private readonly service: string,
109+
private readonly serviceName: string,
110+
private readonly errors: ErrorMap<ErrorCode>
111+
) {}
140112

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

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-
}
117+
const message = template ? replaceTemplate(template, data) : 'Error';
152118

153119
// 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;
120+
const fullMessage = `${this.serviceName}: ${message} (${fullCode}).`;
121+
122+
// Keys with an underscore at the end of their name are not included in
123+
// error.data for some reason.
124+
const error = new FirebaseError(fullCode, fullMessage);
125+
// TODO: Replace with Object.entries when lib is updated to es2017.
126+
for (const key of Object.keys(data)) {
127+
if (key.slice(-1) !== '_') {
128+
if (key in error) {
129+
console.warn(
130+
`Overwriting FirebaseError base field "${key}" can cause unexpected behavior.`
131+
);
132+
}
133+
error[key] = data[key];
162134
}
163-
(err as any)[prop] = data[prop];
164135
}
165-
166-
return err;
136+
return error;
167137
}
168138
}
139+
140+
function replaceTemplate(template: string, data: ErrorData): string {
141+
return template.replace(PATTERN, (_, key) => {
142+
const value = data[key];
143+
return value != null ? value.toString() : `<${key}?>`;
144+
});
145+
}
146+
147+
const PATTERN = /\{\$([^}]+)}/g;

0 commit comments

Comments
 (0)