Skip to content

Commit

Permalink
Override Backbone's trigger with one that catches exceptions
Browse files Browse the repository at this point in the history
Model operations are vulnerable to exceptions thrown by event handlers.
Because this can interrupt really important data operations, it's better
to let the operation continue and log the error. In all likelihood it's
a view-related problem, and that shouldn't cause any data operation to
fail.

FREEBIE
  • Loading branch information
scottnonnenberg committed Aug 4, 2017
1 parent 700272c commit cc2c3ed
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 9 deletions.
1 change: 1 addition & 0 deletions background.html
Expand Up @@ -651,6 +651,7 @@ <h2 class='number'></h2>
</script>

<script type='text/javascript' src='js/components.js'></script>
<script type='text/javascript' src='js/reliable_trigger.js'></script>
<script type='text/javascript' src='js/database.js'></script>
<script type='text/javascript' src='js/debugLog.js'></script>
<script type='text/javascript' src='js/storage.js'></script>
Expand Down
76 changes: 67 additions & 9 deletions js/reliable_trigger.js
@@ -1,5 +1,14 @@
(function () {
// Pure copy from Backbone.
// Note: this is all the code required to customize Backbone's trigger() method to make
// it resilient to exceptions thrown by event handlers. Indentation and code styles
// were kept inline with the Backbone implementation for easier diffs.

// The changes are:
// 1. added 'name' parameter to triggerEvents to give it access to the current event name
// 2. added try/catch handlers to triggerEvents with error logging inside every while loop

// And of course, we update the protoypes of Backbone.Model/Backbone.View as well as
// Backbone.Events itself

// jscs:disable

Expand Down Expand Up @@ -39,14 +48,62 @@
// A difficult-to-believe, but optimized internal dispatch function for
// triggering events. Tries to keep the usual cases speedy (most internal
// Backbone events have 3 arguments).
var triggerEvents = function(events, args) {
var triggerEvents = function(events, name, args) {
var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
var logError = function(error) {
console.log('Model caught error triggering', name, 'event:', error && error.stack ? error.stack : error);
};
switch (args.length) {
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
case 0:
while (++i < l) {
try {
(ev = events[i]).callback.call(ev.ctx);
}
catch (error) {
logError(error);
}
}
return;
case 1:
while (++i < l) {
try {
(ev = events[i]).callback.call(ev.ctx, a1);
}
catch (error) {
logError(error);
}
}
return;
case 2:
while (++i < l) {
try {
(ev = events[i]).callback.call(ev.ctx, a1, a2);
}
catch (error) {
logError(error);
}
}
return;
case 3:
while (++i < l) {
try {
(ev = events[i]).callback.call(ev.ctx, a1, a2, a3);
}
catch (error) {
logError(error);
}
}
return;
default:
while (++i < l) {
try {
(ev = events[i]).callback.apply(ev.ctx, args);
}
catch (error) {
logError(error);
}
}
return;
}
};

Expand All @@ -60,8 +117,9 @@
if (!eventsApi(this, 'trigger', name, args)) return this;
var events = this._events[name];
var allEvents = this._events.all;
if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(allEvents, arguments);
if (events) triggerEvents(events, name, args);
if (allEvents) triggerEvents(allEvents, name, arguments);
return this;
};
})();

2 changes: 2 additions & 0 deletions test/index.html
Expand Up @@ -550,6 +550,7 @@ <h3>{{ message }}</h3>
</script>

<script type="text/javascript" src="../js/components.js"></script>
<script type="text/javascript" src="../js/reliable_trigger.js" data-cover></script>
<script type="text/javascript" src="test.js"></script>

<script type='text/javascript' src='../js/registration.js' data-cover></script>
Expand Down Expand Up @@ -624,6 +625,7 @@ <h3>{{ message }}</h3>
<script type="text/javascript" src="storage_test.js"></script>
<script type="text/javascript" src="keychange_listener_test.js"></script>
<script type="text/javascript" src="emoji_util_test.js"></script>
<script type="text/javascript" src="reliable_trigger_test.js"></script>

<script type="text/javascript" src="fixtures.js"></script>
<script type="text/javascript" src="fixtures_test.js"></script>
Expand Down
147 changes: 147 additions & 0 deletions test/reliable_trigger_test.js
@@ -0,0 +1,147 @@
'use strict';

describe('ReliableTrigger', function() {
describe('trigger', function() {
var Model, model;

before(function() {
Model = Backbone.Model;
});

beforeEach(function() {
model = new Model();
});

it('returns successfully if this._events is falsey', function() {
model._events = null;
model.trigger('click');
});
it('handles map of events to trigger', function() {
var a = 0, b = 0;
model.on('a', function(arg) {
a = arg;
});
model.on('b', function(arg) {
b = arg;
});

model.trigger({
a: 1,
b: 2
});

assert.strictEqual(a, 1);
assert.strictEqual(b, 2);
});
it('handles space-separated list of events to trigger', function() {
var a = false, b = false;
model.on('a', function() {
a = true;
});
model.on('b', function() {
b = true;
});

model.trigger('a b');

assert.strictEqual(a, true);
assert.strictEqual(b, true);
});
it('calls all clients registered for "all" event', function() {
var count = 0;
model.on('all', function() {
count += 1;
});

model.trigger('left');
model.trigger('right');

assert.strictEqual(count, 2);
});
it('calls all clients registered for target event', function() {
var a = false, b = false;
model.on('event', function() {
a = true;
});
model.on('event', function() {
b = true;
});

model.trigger('event');

assert.strictEqual(a, true);
assert.strictEqual(b, true);
});
it('successfully returns and calls all clients even if first failed', function() {
var a = false, b = false;
model.on('event', function() {
a = true;
throw new Error('a is set, but exception is thrown');
});
model.on('event', function() {
b = true;
});

model.trigger('event');

assert.strictEqual(a, true);
assert.strictEqual(b, true);
});
it('calls clients with no args', function() {
var called = false;
model.on('event', function() {
called = true;
});

model.trigger('event');

assert.strictEqual(called, true);
});
it('calls clients with 1 arg', function() {
var args;
model.on('event', function() {
args = arguments;
});

model.trigger('event', 1);

assert.strictEqual(args[0], 1);
});
it('calls clients with 2 args', function() {
var args;
model.on('event', function() {
args = arguments;
});

model.trigger('event', 1, 2);

assert.strictEqual(args[0], 1);
assert.strictEqual(args[1], 2);
});
it('calls clients with 3 args', function() {
var args;
model.on('event', function() {
args = arguments;
});

model.trigger('event', 1, 2, 3);

assert.strictEqual(args[0], 1);
assert.strictEqual(args[1], 2);
assert.strictEqual(args[2], 3);
});
it('calls clients with 4+ args', function() {
var args;
model.on('event', function() {
args = arguments;
});

model.trigger('event', 1, 2, 3, 4);

assert.strictEqual(args[0], 1);
assert.strictEqual(args[1], 2);
assert.strictEqual(args[2], 3);
assert.strictEqual(args[3], 4);
});
});
});

0 comments on commit cc2c3ed

Please sign in to comment.