diff --git a/src/Angular.js b/src/Angular.js index aab712d3e1ee..f502475a4ed3 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -967,8 +967,20 @@ function toJsonReplacer(key, value) { * @param {boolean=} pretty If set to true, the JSON output will contain newlines and whitespace. * @returns {string|undefined} JSON-ified string representing `obj`. */ -function toJson(obj, pretty) { +function toJson(obj, pretty, options) { if (typeof obj === 'undefined') return undefined; + + if (!options) options = {}; + + if (options.suppressExceptions) { + try { + return JSON.stringify(obj, toJsonReplacer, pretty ? ' ' : null); + } catch (e) { + // in case JSON.stringify throws, suppress error and return undefined + return undefined; + } + } + return JSON.stringify(obj, toJsonReplacer, pretty ? ' ' : null); } diff --git a/src/minErr.js b/src/minErr.js index 96171915b7d7..d89bfde0cc18 100644 --- a/src/minErr.js +++ b/src/minErr.js @@ -59,7 +59,7 @@ function minErr(module, ErrorConstructor) { } else if (typeof arg === 'undefined') { return 'undefined'; } else if (typeof arg !== 'string') { - return toJson(arg); + return toJson(arg, false, {suppressExceptions: true}); } return arg; } diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index f0c2660faa5c..b97bc7604b29 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -366,7 +366,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { }); throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}", - expression, trackById, toJson(value)); + expression, trackById, toJson(value, false, {suppressExceptions: true})); } else { // new never before seen block nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined}; diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index df8ddcf6d4cc..28957b4bd134 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -776,7 +776,8 @@ function $RootScopeProvider() { logMsg = (isFunction(watch.exp)) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp; - logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); + logMsg += '; newVal: ' + toJson(value, false, {suppressExceptions: true}) + + '; oldVal: ' + toJson(last, false, {suppressExceptions: true}); watchLog[logIdx].push(logMsg); } } else if (watch === lastDirtyWatch) { @@ -810,7 +811,7 @@ function $RootScopeProvider() { throw $rootScopeMinErr('infdig', '{0} $digest() iterations reached. Aborting!\n' + 'Watchers fired in the last 5 iterations: {1}', - TTL, toJson(watchLog)); + TTL, toJson(watchLog, false, {suppressExceptions: true})); } } while (dirty || asyncQueue.length); diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 5ab87771eb58..922e672b7081 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -1219,6 +1219,33 @@ describe('angular', function() { it('should serialize undefined as undefined', function() { expect(toJson(undefined)).toEqual(undefined); }); + + it('should allow JSON.stringify to throw exceptions by default', function() { + // create objects with references to each other (circular reference) + // passing such object to JSON.stringify throws a TypeError + var a = {}, b = {test: a}; + a.test = b; + + // prepare toJson function with correct arguments for this test + var toJsonSuppressedTest = toJson.bind(undefined, a); + + // it should throw a TypeError + expect(toJsonSuppressedTest).toThrow(); + }); + + it('should not throw exceptions if suppressExceptions option is passed', function() { + // create objects with references to each other (circular reference) + // passing such object to JSON.stringify throws a TypeError + var a = {}, b = {test: a}; + a.test = b; + + // prepare toJson function with correct arguments for this test + var toJsonSuppressedTest = toJson.bind(undefined, a, false, {suppressExceptions: true}); + + // it shouldn't throw a TypeError and it should return undefined + expect(toJsonSuppressedTest).not.toThrow(); + expect(toJsonSuppressedTest()).toBeUndefined(); + }); }); describe('isElement', function() {