blob: 3d8b0042686fc356081bfa2ad7e4497747902d62 [file] [log] [blame]
// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Error handling utilities.
*
*/
goog.provide('goog.debug.ErrorHandler');
goog.provide('goog.debug.ErrorHandler.ProtectedFunctionError');
goog.require('goog.Disposable');
goog.require('goog.asserts');
goog.require('goog.debug');
goog.require('goog.debug.EntryPointMonitor');
goog.require('goog.debug.Error');
goog.require('goog.debug.Trace');
/**
* The ErrorHandler can be used to to wrap functions with a try/catch
* statement. If an exception is thrown, the given error handler function will
* be called.
*
* When this object is disposed, it will stop handling exceptions and tracing.
* It will also try to restore window.setTimeout and window.setInterval
* if it wrapped them. Notice that in the general case, it is not technically
* possible to remove the wrapper, because functions have no knowledge of
* what they have been assigned to. So the app is responsible for other
* forms of unwrapping.
*
* @param {Function} handler Handler for exceptions.
* @constructor
* @extends {goog.Disposable}
* @implements {goog.debug.EntryPointMonitor}
*/
goog.debug.ErrorHandler = function(handler) {
goog.debug.ErrorHandler.base(this, 'constructor');
/**
* Handler for exceptions, which can do logging, reporting, etc.
* @type {Function}
* @private
*/
this.errorHandlerFn_ = handler;
/**
* Whether errors should be wrapped in
* goog.debug.ErrorHandler.ProtectedFunctionError before rethrowing.
* @type {boolean}
* @private
*/
this.wrapErrors_ = true; // TODO(user) Change default.
/**
* Whether to add a prefix to all error messages. The prefix is
* goog.debug.ErrorHandler.ProtectedFunctionError.MESSAGE_PREFIX. This option
* only has an effect if this.wrapErrors_ is set to false.
* @type {boolean}
* @private
*/
this.prefixErrorMessages_ = false;
};
goog.inherits(goog.debug.ErrorHandler, goog.Disposable);
/**
* Whether to add tracers when instrumenting entry points.
* @type {boolean}
* @private
*/
goog.debug.ErrorHandler.prototype.addTracersToProtectedFunctions_ = false;
/**
* Enable tracers when instrumenting entry points.
* @param {boolean} newVal See above.
*/
goog.debug.ErrorHandler.prototype.setAddTracersToProtectedFunctions =
function(newVal) {
this.addTracersToProtectedFunctions_ = newVal;
};
/** @override */
goog.debug.ErrorHandler.prototype.wrap = function(fn) {
return this.protectEntryPoint(goog.asserts.assertFunction(fn));
};
/** @override */
goog.debug.ErrorHandler.prototype.unwrap = function(fn) {
goog.asserts.assertFunction(fn);
return fn[this.getFunctionIndex_(false)] || fn;
};
/**
* Private helper function to return a span that can be clicked on to display
* an alert with the current stack trace. Newlines are replaced with a
* placeholder so that they will not be html-escaped.
* @param {string} stackTrace The stack trace to create a span for.
* @return {string} A span which can be clicked on to show the stack trace.
* @private
*/
goog.debug.ErrorHandler.prototype.getStackTraceHolder_ = function(stackTrace) {
var buffer = [];
buffer.push('##PE_STACK_START##');
buffer.push(stackTrace.replace(/(\r\n|\r|\n)/g, '##STACK_BR##'));
buffer.push('##PE_STACK_END##');
return buffer.join('');
};
/**
* Get the index for a function. Used for internal indexing.
* @param {boolean} wrapper True for the wrapper; false for the wrapped.
* @return {string} The index where we should store the function in its
* wrapper/wrapped function.
* @private
*/
goog.debug.ErrorHandler.prototype.getFunctionIndex_ = function(wrapper) {
return (wrapper ? '__wrapper_' : '__protected_') + goog.getUid(this) + '__';
};
/**
* Installs exception protection for an entry point function. When an exception
* is thrown from a protected function, a handler will be invoked to handle it.
*
* @param {Function} fn An entry point function to be protected.
* @return {!Function} A protected wrapper function that calls the entry point
* function.
*/
goog.debug.ErrorHandler.prototype.protectEntryPoint = function(fn) {
var protectedFnName = this.getFunctionIndex_(true);
if (!fn[protectedFnName]) {
var wrapper = fn[protectedFnName] = this.getProtectedFunction(fn);
wrapper[this.getFunctionIndex_(false)] = fn;
}
return fn[protectedFnName];
};
/**
* Helps {@link #protectEntryPoint} by actually creating the protected
* wrapper function, after {@link #protectEntryPoint} determines that one does
* not already exist for the given function. Can be overriden by subclasses
* that may want to implement different error handling, or add additional
* entry point hooks.
* @param {!Function} fn An entry point function to be protected.
* @return {!Function} protected wrapper function.
* @protected
*/
goog.debug.ErrorHandler.prototype.getProtectedFunction = function(fn) {
var that = this;
var tracers = this.addTracersToProtectedFunctions_;
if (tracers) {
var stackTrace = goog.debug.getStacktraceSimple(15);
}
var googDebugErrorHandlerProtectedFunction = function() {
if (that.isDisposed()) {
return fn.apply(this, arguments);
}
if (tracers) {
var tracer = goog.debug.Trace.startTracer('protectedEntryPoint: ' +
that.getStackTraceHolder_(stackTrace));
}
try {
return fn.apply(this, arguments);
} catch (e) {
that.errorHandlerFn_(e);
if (!that.wrapErrors_) {
// Add the prefix to the existing message.
if (that.prefixErrorMessages_) {
if (typeof e === 'object') {
e.message =
goog.debug.ErrorHandler.ProtectedFunctionError.MESSAGE_PREFIX +
e.message;
} else {
e = goog.debug.ErrorHandler.ProtectedFunctionError.MESSAGE_PREFIX +
e;
}
}
if (goog.DEBUG) {
// Work around for https://code.google.com/p/v8/issues/detail?id=2625
// and https://code.google.com/p/chromium/issues/detail?id=237059
// Custom errors and errors with custom stack traces show the wrong
// stack trace
// If it has a stack and Error.captureStackTrace is supported (only
// supported in V8 as of May 2013) log the stack to the console.
if (e && e.stack && Error.captureStackTrace &&
goog.global['console']) {
goog.global['console']['error'](e.message, e.stack);
}
}
// Re-throw original error. This is great for debugging as it makes
// browser JS dev consoles show the correct error and stack trace.
throw e;
}
// Re-throw it since this may be expected by the caller.
throw new goog.debug.ErrorHandler.ProtectedFunctionError(e);
} finally {
if (tracers) {
goog.debug.Trace.stopTracer(tracer);
}
}
};
googDebugErrorHandlerProtectedFunction[this.getFunctionIndex_(false)] = fn;
return googDebugErrorHandlerProtectedFunction;
};
// TODO(mknichel): Allow these functions to take in the window to protect.
/**
* Installs exception protection for window.setTimeout to handle exceptions.
*/
goog.debug.ErrorHandler.prototype.protectWindowSetTimeout =
function() {
this.protectWindowFunctionsHelper_('setTimeout');
};
/**
* Install exception protection for window.setInterval to handle exceptions.
*/
goog.debug.ErrorHandler.prototype.protectWindowSetInterval =
function() {
this.protectWindowFunctionsHelper_('setInterval');
};
/**
* Install exception protection for window.requestAnimationFrame to handle
* exceptions.
*/
goog.debug.ErrorHandler.prototype.protectWindowRequestAnimationFrame =
function() {
var win = goog.getObjectByName('window');
var fnNames = [
'requestAnimationFrame',
'mozRequestAnimationFrame',
'webkitAnimationFrame',
'msRequestAnimationFrame'
];
for (var i = 0; i < fnNames.length; i++) {
var fnName = fnNames[i];
if (fnNames[i] in win) {
this.protectWindowFunctionsHelper_(fnName);
}
}
};
/**
* Helper function for protecting a function that causes a function to be
* asynchronously called, for example setTimeout or requestAnimationFrame.
* @param {string} fnName The name of the function to protect.
* @private
*/
goog.debug.ErrorHandler.prototype.protectWindowFunctionsHelper_ =
function(fnName) {
var win = goog.getObjectByName('window');
var originalFn = win[fnName];
var that = this;
win[fnName] = function(fn, time) {
// Don't try to protect strings. In theory, we could try to globalEval
// the string, but this seems to lead to permission errors on IE6.
if (goog.isString(fn)) {
fn = goog.partial(goog.globalEval, fn);
}
fn = that.protectEntryPoint(fn);
// IE doesn't support .call for setInterval/setTimeout, but it
// also doesn't care what "this" is, so we can just call the
// original function directly
if (originalFn.call) {
return originalFn.call(this, fn, time);
} else {
return originalFn(fn, time);
}
};
win[fnName][this.getFunctionIndex_(false)] = originalFn;
};
/**
* Set whether to wrap errors that occur in protected functions in a
* goog.debug.ErrorHandler.ProtectedFunctionError.
* @param {boolean} wrapErrors Whether to wrap errors.
*/
goog.debug.ErrorHandler.prototype.setWrapErrors = function(wrapErrors) {
this.wrapErrors_ = wrapErrors;
};
/**
* Set whether to add a prefix to all error messages that occur in protected
* functions.
* @param {boolean} prefixErrorMessages Whether to add a prefix to error
* messages.
*/
goog.debug.ErrorHandler.prototype.setPrefixErrorMessages =
function(prefixErrorMessages) {
this.prefixErrorMessages_ = prefixErrorMessages;
};
/** @override */
goog.debug.ErrorHandler.prototype.disposeInternal = function() {
// Try to unwrap window.setTimeout and window.setInterval.
var win = goog.getObjectByName('window');
win.setTimeout = this.unwrap(win.setTimeout);
win.setInterval = this.unwrap(win.setInterval);
goog.debug.ErrorHandler.base(this, 'disposeInternal');
};
/**
* Error thrown to the caller of a protected entry point if the entry point
* throws an error.
* @param {*} cause The error thrown by the entry point.
* @constructor
* @extends {goog.debug.Error}
* @final
*/
goog.debug.ErrorHandler.ProtectedFunctionError = function(cause) {
var message = goog.debug.ErrorHandler.ProtectedFunctionError.MESSAGE_PREFIX +
(cause && cause.message ? String(cause.message) : String(cause));
goog.debug.ErrorHandler.ProtectedFunctionError.base(
this, 'constructor', message);
/**
* The error thrown by the entry point.
* @type {*}
*/
this.cause = cause;
var stack = cause && cause.stack;
if (stack && goog.isString(stack)) {
this.stack = /** @type {string} */ (stack);
}
};
goog.inherits(goog.debug.ErrorHandler.ProtectedFunctionError, goog.debug.Error);
/**
* Text to prefix the message with.
* @type {string}
*/
goog.debug.ErrorHandler.ProtectedFunctionError.MESSAGE_PREFIX =
'Error in protected function: ';