Skip to content

Commit 3fb5cda

Browse files
Steve Orvellkevinpschaaf
Steve Orvell
authored andcommitted
Handle exceptions during update so that they do not break subsequent updates (#607)
* Handle exceptions during update so that they do not break subsequent updates Fixes #262. Catch exceptions during update and ensure the element marks itself as finished updating. Note, this change also ensures the `updateComplete` promise is rejected if there's an exception during update. * update tests consistently to use helper to add/remove test elements * Fix lint error * Address review feedback * Address review feedback. * Refines error handling based on review * `shouldUpdate`/`update` are try/caught so that the element state is ok for further updates if an exception is thrown * `firstUpdated`/`updated` are not called if there's an update exception * `performUpdate` is try/caught only to reject the update promise. This is done to handle an override producing an exception. * a private version `_requestUpdate` is called in the property setter to avoid accessing the overridable `updateComplete` promise. * Added additional js doc comments. * Simplify promise check and fix lint errors
1 parent 5b21b7e commit 3fb5cda

File tree

4 files changed

+349
-43
lines changed

4 files changed

+349
-43
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1919
* `LitElement.renderRoot` is now `public readonly` instead of `protected`.
2020

2121
### Fixed
22+
* Exceptions generated during update/render do not block subsequent updates ([#262](https://github.com/Polymer/lit-element/issues/262)).
2223
* Initial update is scheduled at construction time rather than connected time ([#594](https://github.com/Polymer/lit-element/issues/594)).
2324
* A reflecting property set immediately after a corresponding attribute
2425
now reflects properly ([#592](https://github.com/Polymer/lit-element/issues/592)).

src/lib/updating-element.ts

Lines changed: 72 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ export abstract class UpdatingElement extends HTMLElement {
303303
const oldValue = (this as any)[name];
304304
// tslint:disable-next-line:no-any no symbol in index
305305
(this as any)[key] = value;
306-
this.requestUpdate(name, oldValue);
306+
this._requestUpdate(name, oldValue);
307307
},
308308
configurable: true,
309309
enumerable: true
@@ -442,7 +442,7 @@ export abstract class UpdatingElement extends HTMLElement {
442442
protected initialize() {
443443
this._saveInstanceProperties();
444444
// ensures first update will be caught by an early access of `updateComplete`
445-
this.requestUpdate();
445+
this._requestUpdate();
446446
}
447447

448448
/**
@@ -565,19 +565,11 @@ export abstract class UpdatingElement extends HTMLElement {
565565
}
566566

567567
/**
568-
* Requests an update which is processed asynchronously. This should
569-
* be called when an element should update based on some state not triggered
570-
* by setting a property. In this case, pass no arguments. It should also be
571-
* called when manually implementing a property setter. In this case, pass the
572-
* property `name` and `oldValue` to ensure that any configured property
573-
* options are honored. Returns the `updateComplete` Promise which is resolved
574-
* when the update completes.
575-
*
576-
* @param name {PropertyKey} (optional) name of requesting property
577-
* @param oldValue {any} (optional) old value of requesting property
578-
* @returns {Promise} A Promise that is resolved when the update completes.
568+
* This private version of `requestUpdate` does not access or return the
569+
* `updateComplete` promise. This promise can be overridden and is therefore
570+
* not free to access.
579571
*/
580-
requestUpdate(name?: PropertyKey, oldValue?: unknown) {
572+
private _requestUpdate(name?: PropertyKey, oldValue?: unknown) {
581573
let shouldRequestUpdate = true;
582574
// If we have a property key, perform property update steps.
583575
if (name !== undefined) {
@@ -608,6 +600,23 @@ export abstract class UpdatingElement extends HTMLElement {
608600
if (!this._hasRequestedUpdate && shouldRequestUpdate) {
609601
this._enqueueUpdate();
610602
}
603+
}
604+
605+
/**
606+
* Requests an update which is processed asynchronously. This should
607+
* be called when an element should update based on some state not triggered
608+
* by setting a property. In this case, pass no arguments. It should also be
609+
* called when manually implementing a property setter. In this case, pass the
610+
* property `name` and `oldValue` to ensure that any configured property
611+
* options are honored. Returns the `updateComplete` Promise which is resolved
612+
* when the update completes.
613+
*
614+
* @param name {PropertyKey} (optional) name of requesting property
615+
* @param oldValue {any} (optional) old value of requesting property
616+
* @returns {Promise} A Promise that is resolved when the update completes.
617+
*/
618+
requestUpdate(name?: PropertyKey, oldValue?: unknown) {
619+
this._requestUpdate(name, oldValue);
611620
return this.updateComplete;
612621
}
613622

@@ -617,27 +626,37 @@ export abstract class UpdatingElement extends HTMLElement {
617626
private async _enqueueUpdate() {
618627
// Mark state updating...
619628
this._updateState = this._updateState | STATE_UPDATE_REQUESTED;
620-
let resolve: (r: boolean) => void;
629+
let resolve!: (r: boolean) => void;
630+
let reject!: (e: Error) => void;
621631
const previousUpdatePromise = this._updatePromise;
622-
this._updatePromise = new Promise((res) => resolve = res);
623-
// Ensure any previous update has resolved before updating.
624-
// This `await` also ensures that property changes are batched.
625-
await previousUpdatePromise;
632+
this._updatePromise = new Promise((res, rej) => {
633+
resolve = res;
634+
reject = rej;
635+
});
636+
try {
637+
// Ensure any previous update has resolved before updating.
638+
// This `await` also ensures that property changes are batched.
639+
await previousUpdatePromise;
640+
} catch (e) {
641+
// Ignore any previous errors. We only care that the previous cycle is
642+
// done. Any error should have been handled in the previous update.
643+
}
626644
// Make sure the element has connected before updating.
627645
if (!this._hasConnected) {
628646
await new Promise((res) => this._hasConnectedResolver = res);
629647
}
630-
// Allow `performUpdate` to be asynchronous to enable scheduling of updates.
631-
const result = this.performUpdate();
632-
// Note, this is to avoid delaying an additional microtask unless we need
633-
// to.
634-
if (result != null &&
635-
typeof (result as PromiseLike<unknown>).then === 'function') {
636-
await result;
648+
try {
649+
const result = this.performUpdate();
650+
// If `performUpdate` returns a Promise, we await it. This is done to
651+
// enable coordinating updates with a scheduler. Note, the result is
652+
// checked to avoid delaying an additional microtask unless we need to.
653+
if (result != null) {
654+
await result;
655+
}
656+
} catch (e) {
657+
reject(e);
637658
}
638-
// TypeScript can't tell that we've initialized resolve.
639-
// tslint:disable-next-line:no-unnecessary-type-assertion
640-
resolve!(!this._hasRequestedUpdate);
659+
resolve(!this._hasRequestedUpdate);
641660
}
642661

643662
private get _hasConnected() {
@@ -653,10 +672,13 @@ export abstract class UpdatingElement extends HTMLElement {
653672
}
654673

655674
/**
656-
* Performs an element update.
675+
* Performs an element update. Note, if an exception is thrown during the
676+
* update, `firstUpdated` and `updated` will not be called.
657677
*
658-
* You can override this method to change the timing of updates. For instance,
659-
* to schedule updates to occur just before the next frame:
678+
* You can override this method to change the timing of updates. If this
679+
* method is overridden, `super.performUpdate()` must be called.
680+
*
681+
* For instance, to schedule updates to occur just before the next frame:
660682
*
661683
* ```
662684
* protected async performUpdate(): Promise<unknown> {
@@ -670,17 +692,28 @@ export abstract class UpdatingElement extends HTMLElement {
670692
if (this._instanceProperties) {
671693
this._applyInstanceProperties();
672694
}
673-
if (this.shouldUpdate(this._changedProperties)) {
674-
const changedProperties = this._changedProperties;
675-
this.update(changedProperties);
695+
let shouldUpdate = false;
696+
const changedProperties = this._changedProperties;
697+
try {
698+
shouldUpdate = this.shouldUpdate(changedProperties);
699+
if (shouldUpdate) {
700+
this.update(changedProperties);
701+
}
702+
} catch (e) {
703+
// Prevent `firstUpdated` and `updated` from running when there's an
704+
// update exception.
705+
shouldUpdate = false;
706+
throw e;
707+
} finally {
708+
// Ensure element can accept additional updates after an exception.
676709
this._markUpdated();
710+
}
711+
if (shouldUpdate) {
677712
if (!(this._updateState & STATE_HAS_UPDATED)) {
678713
this._updateState = this._updateState | STATE_HAS_UPDATED;
679714
this.firstUpdated(changedProperties);
680715
}
681716
this.updated(changedProperties);
682-
} else {
683-
this._markUpdated();
684717
}
685718
}
686719

@@ -693,7 +726,8 @@ export abstract class UpdatingElement extends HTMLElement {
693726
* Returns a Promise that resolves when the element has completed updating.
694727
* The Promise value is a boolean that is `true` if the element completed the
695728
* update without triggering another update. The Promise result is `false` if
696-
* a property was set inside `updated()`. This getter can be implemented to
729+
* a property was set inside `updated()`. If the Promise is rejected, an
730+
* exception was thrown during the update. This getter can be implemented to
697731
* await additional state. For example, it is sometimes useful to await a
698732
* rendered element before fulfilling this Promise. To do this, first await
699733
* `super.updateComplete` then any subsequent state.

0 commit comments

Comments
 (0)