// 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) { // Don't re-report errors that have already been handled by this code. var MESSAGE_PREFIX = goog.debug.ErrorHandler.ProtectedFunctionError.MESSAGE_PREFIX; if ((e && typeof e === 'object' && e.message && e.message.indexOf(MESSAGE_PREFIX) == 0) || (typeof e === 'string' && e.indexOf(MESSAGE_PREFIX) == 0)) { return; } that.errorHandlerFn_(e); if (!that.wrapErrors_) { // Add the prefix to the existing message. if (that.prefixErrorMessages_) { if (typeof e === 'object' && e && 'message' in e) { e.message = MESSAGE_PREFIX + e.message; } else { e = 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); } arguments[0] = 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.apply) { return originalFn.apply(this, arguments); } else { var callback = fn; if (arguments.length > 2) { var args = Array.prototype.slice.call(arguments, 2); callback = function() { fn.apply(this, args); }; } return originalFn(callback, 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: ';