Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
Runtime error with nested components, {{#if}}s, and {{yield}}
Very similar to https://github.com/sveltejs/svelte/issues/625 - observed in Svelte 1.22.3.

I got a reproduction in the REPL [here](https://svelte.technology/repl?version=1.22.3&gist=6c615c874cab9369d44a7dafdeb08ebc) but then downloaded it locally to fiddle with it some more, so you can find an even slimmer reproduction in https://github.com/TehShrike/svelte-1-22-3-nested-yield-issue-repro

When the state changes and causes some elements to be cleaned up, an error is thrown `Uncaught TypeError: Cannot read property 'unmount' of null`

Seems to happen when there is a loop that contains a component with `{{yield}}` inside of an `{{#if}}` block, but only when there is another Svelte component nested inside of the `yield`ing component.


Discussion:
Hitting the same error in one of my projects using Svelte when I upgraded from a 5 month old version. Only difference is that while it's the same error (calling unmount on null fragment) my project doesn't use loops. Your example is much smaller than mine, so I'll try to debug it and figure out what's going on.
yeah, it may be possible to trigger the issue with a `destroy()` or otherwise causing the nested components to not be needed any more.  I only spent a little time slimming down the reproduction case.
Found it: https://github.com/sveltejs/svelte/blob/fe75570b910cc3e0cf6df55f8264dc50caafb2a4/src/generators/dom/visitors/IfBlock.ts#L274-L278

`${name}.unmount` needs to be added to the unmount block.
Is this issue fixed? Tried to run the Modal example from the guide, but got the same error. Tried it locally, and also in the REPL. I am on version 1.23.4.

Resolving patch:
diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts
--- a/src/generators/dom/index.ts
+++ b/src/generators/dom/index.ts
@@ -211,7 +211,7 @@ export default function dom(
 			this._root = options._root || this;
 			this._yield = options._yield;
 
-			this._torndown = false;
+			this._destroyed = false;
 			${generator.stylesheet.hasStyles &&
 				options.css !== false &&
 				`if ( !document.getElementById( '${generator.stylesheet.id}-style' ) ) @add_css();`}
@@ -264,6 +264,7 @@ export default function dom(
 		};
 
 		${name}.prototype.teardown = ${name}.prototype.destroy = function destroy ( detach ) {
+			if ( this._destroyed ) return${options.dev && ` console.warn( 'Component was already destroyed' )`};
 			this.fire( 'destroy' );
 			${templateProperties.ondestroy && `@template.ondestroy.call( this );`}
 
@@ -272,7 +273,7 @@ export default function dom(
 			this._fragment = null;
 
 			this._state = {};
-			this._torndown = true;
+			this._destroyed = true;
 		};
 	`);
 


Test patch:
diff --git a/test/js/samples/collapses-text-around-comments/expected-bundle.js b/test/js/samples/collapses-text-around-comments/expected-bundle.js
--- a/test/js/samples/collapses-text-around-comments/expected-bundle.js
+++ b/test/js/samples/collapses-text-around-comments/expected-bundle.js
@@ -201,7 +201,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 	if ( !document.getElementById( 'svelte-3590263702-style' ) ) add_css();
 
 	this._fragment = create_main_fragment( this._state, this );
@@ -223,6 +223,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -230,7 +231,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
diff --git a/test/js/samples/collapses-text-around-comments/expected.js b/test/js/samples/collapses-text-around-comments/expected.js
--- a/test/js/samples/collapses-text-around-comments/expected.js
+++ b/test/js/samples/collapses-text-around-comments/expected.js
@@ -66,7 +66,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 	if ( !document.getElementById( 'svelte-3590263702-style' ) ) add_css();
 
 	this._fragment = create_main_fragment( this._state, this );
@@ -88,6 +88,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -95,7 +96,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
\ No newline at end of file
diff --git a/test/js/samples/computed-collapsed-if/expected-bundle.js b/test/js/samples/computed-collapsed-if/expected-bundle.js
--- a/test/js/samples/computed-collapsed-if/expected-bundle.js
+++ b/test/js/samples/computed-collapsed-if/expected-bundle.js
@@ -155,7 +155,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 
 	this._fragment = create_main_fragment( this._state, this );
 
@@ -176,6 +176,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -183,7 +184,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
diff --git a/test/js/samples/computed-collapsed-if/expected.js b/test/js/samples/computed-collapsed-if/expected.js
--- a/test/js/samples/computed-collapsed-if/expected.js
+++ b/test/js/samples/computed-collapsed-if/expected.js
@@ -44,7 +44,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 
 	this._fragment = create_main_fragment( this._state, this );
 
@@ -65,6 +65,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -72,7 +73,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
\ No newline at end of file
diff --git a/test/js/samples/css-media-query/expected-bundle.js b/test/js/samples/css-media-query/expected-bundle.js
--- a/test/js/samples/css-media-query/expected-bundle.js
+++ b/test/js/samples/css-media-query/expected-bundle.js
@@ -181,7 +181,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 	if ( !document.getElementById( 'svelte-2363328337-style' ) ) add_css();
 
 	this._fragment = create_main_fragment( this._state, this );
@@ -202,6 +202,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -209,7 +210,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
diff --git a/test/js/samples/css-media-query/expected.js b/test/js/samples/css-media-query/expected.js
--- a/test/js/samples/css-media-query/expected.js
+++ b/test/js/samples/css-media-query/expected.js
@@ -50,7 +50,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 	if ( !document.getElementById( 'svelte-2363328337-style' ) ) add_css();
 
 	this._fragment = create_main_fragment( this._state, this );
@@ -71,6 +71,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -78,7 +79,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
\ No newline at end of file
diff --git a/test/js/samples/each-block-changed-check/expected-bundle.js b/test/js/samples/each-block-changed-check/expected-bundle.js
--- a/test/js/samples/each-block-changed-check/expected-bundle.js
+++ b/test/js/samples/each-block-changed-check/expected-bundle.js
@@ -301,7 +301,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 
 	this._fragment = create_main_fragment( this._state, this );
 
@@ -322,6 +322,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -329,7 +330,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js
--- a/test/js/samples/each-block-changed-check/expected.js
+++ b/test/js/samples/each-block-changed-check/expected.js
@@ -157,7 +157,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 
 	this._fragment = create_main_fragment( this._state, this );
 
@@ -178,6 +178,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -185,7 +186,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
\ No newline at end of file
diff --git a/test/js/samples/event-handlers-custom/expected-bundle.js b/test/js/samples/event-handlers-custom/expected-bundle.js
--- a/test/js/samples/event-handlers-custom/expected-bundle.js
+++ b/test/js/samples/event-handlers-custom/expected-bundle.js
@@ -190,7 +190,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 
 	this._fragment = create_main_fragment( this._state, this );
 
@@ -210,6 +210,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -217,7 +218,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
diff --git a/test/js/samples/event-handlers-custom/expected.js b/test/js/samples/event-handlers-custom/expected.js
--- a/test/js/samples/event-handlers-custom/expected.js
+++ b/test/js/samples/event-handlers-custom/expected.js
@@ -61,7 +61,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 
 	this._fragment = create_main_fragment( this._state, this );
 
@@ -81,6 +81,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -88,7 +89,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
\ No newline at end of file
diff --git a/test/js/samples/if-block-no-update/expected-bundle.js b/test/js/samples/if-block-no-update/expected-bundle.js
--- a/test/js/samples/if-block-no-update/expected-bundle.js
+++ b/test/js/samples/if-block-no-update/expected-bundle.js
@@ -236,7 +236,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 
 	this._fragment = create_main_fragment( this._state, this );
 
@@ -257,6 +257,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -264,7 +265,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
diff --git a/test/js/samples/if-block-no-update/expected.js b/test/js/samples/if-block-no-update/expected.js
--- a/test/js/samples/if-block-no-update/expected.js
+++ b/test/js/samples/if-block-no-update/expected.js
@@ -101,7 +101,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 
 	this._fragment = create_main_fragment( this._state, this );
 
@@ -122,6 +122,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -129,7 +130,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
\ No newline at end of file
diff --git a/test/js/samples/if-block-simple/expected-bundle.js b/test/js/samples/if-block-simple/expected-bundle.js
--- a/test/js/samples/if-block-simple/expected-bundle.js
+++ b/test/js/samples/if-block-simple/expected-bundle.js
@@ -212,7 +212,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 
 	this._fragment = create_main_fragment( this._state, this );
 
@@ -233,6 +233,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -240,7 +241,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
diff --git a/test/js/samples/if-block-simple/expected.js b/test/js/samples/if-block-simple/expected.js
--- a/test/js/samples/if-block-simple/expected.js
+++ b/test/js/samples/if-block-simple/expected.js
@@ -77,7 +77,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 
 	this._fragment = create_main_fragment( this._state, this );
 
@@ -98,6 +98,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -105,7 +106,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
\ No newline at end of file
diff --git a/test/js/samples/non-imported-component/expected-bundle.js b/test/js/samples/non-imported-component/expected-bundle.js
--- a/test/js/samples/non-imported-component/expected-bundle.js
+++ b/test/js/samples/non-imported-component/expected-bundle.js
@@ -182,7 +182,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 
 	if ( !options._root ) {
 		this._oncreate = [];
@@ -216,6 +216,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -223,7 +224,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
diff --git a/test/js/samples/non-imported-component/expected.js b/test/js/samples/non-imported-component/expected.js
--- a/test/js/samples/non-imported-component/expected.js
+++ b/test/js/samples/non-imported-component/expected.js
@@ -61,7 +61,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 
 	if ( !options._root ) {
 		this._oncreate = [];
@@ -95,6 +95,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -102,7 +103,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
\ No newline at end of file
diff --git a/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js b/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js
--- a/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js
+++ b/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js
@@ -146,7 +146,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 
 	var oncreate = template.oncreate.bind( this );
 
@@ -178,6 +178,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 	template.ondestroy.call( this );
 
@@ -186,7 +187,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
diff --git a/test/js/samples/onrender-onteardown-rewritten/expected.js b/test/js/samples/onrender-onteardown-rewritten/expected.js
--- a/test/js/samples/onrender-onteardown-rewritten/expected.js
+++ b/test/js/samples/onrender-onteardown-rewritten/expected.js
@@ -35,7 +35,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 
 	var oncreate = template.oncreate.bind( this );
 
@@ -67,6 +67,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 	template.ondestroy.call( this );
 
@@ -75,7 +76,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
\ No newline at end of file
diff --git a/test/js/samples/use-elements-as-anchors/expected-bundle.js b/test/js/samples/use-elements-as-anchors/expected-bundle.js
--- a/test/js/samples/use-elements-as-anchors/expected-bundle.js
+++ b/test/js/samples/use-elements-as-anchors/expected-bundle.js
@@ -396,7 +396,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 
 	this._fragment = create_main_fragment( this._state, this );
 
@@ -417,6 +417,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -424,7 +425,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
diff --git a/test/js/samples/use-elements-as-anchors/expected.js b/test/js/samples/use-elements-as-anchors/expected.js
--- a/test/js/samples/use-elements-as-anchors/expected.js
+++ b/test/js/samples/use-elements-as-anchors/expected.js
@@ -261,7 +261,7 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._torndown = false;
+	this._destroyed = false;
 
 	this._fragment = create_main_fragment( this._state, this );
 
@@ -282,6 +282,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
+	if ( this._destroyed ) return;
 	this.fire( 'destroy' );
 
 	if ( detach !== false ) this._fragment.unmount();
@@ -289,7 +290,7 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
 	this._fragment = null;
 
 	this._state = {};
-	this._torndown = true;
+	this._destroyed = true;
 };
 
 export default SvelteComponent;
\ No newline at end of file
diff --git a/test/runtime/samples/destroy-twice/_config.js b/test/runtime/samples/destroy-twice/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/destroy-twice/_config.js
@@ -0,0 +1,6 @@
+export default {
+	test(assert, component) {
+		component.destroy();
+		component.destroy();
+	}
+};
\ No newline at end of file
diff --git a/test/runtime/samples/destroy-twice/main.html b/test/runtime/samples/destroy-twice/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/destroy-twice/main.html
@@ -0,0 +1 @@
+<div/>
\ No newline at end of file
diff --git a/test/runtime/samples/dev-warning-destroy-twice/_config.js b/test/runtime/samples/dev-warning-destroy-twice/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/dev-warning-destroy-twice/_config.js
@@ -0,0 +1,21 @@
+export default {
+	dev: true,
+
+	test(assert, component) {
+		const warn = console.warn; // eslint-disable-line no-console
+
+		const warnings = [];
+		console.warn = warning => { // eslint-disable-line no-console
+			warnings.push(warning);
+		};
+
+		component.destroy();
+		component.destroy();
+
+		assert.deepEqual(warnings, [
+			`Component was already destroyed`
+		]);
+
+		console.warn = warn; // eslint-disable-line no-console
+	}
+};
\ No newline at end of file
diff --git a/test/runtime/samples/dev-warning-destroy-twice/main.html b/test/runtime/samples/dev-warning-destroy-twice/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/dev-warning-destroy-twice/main.html
@@ -0,0 +1 @@
+<div/>
\ No newline at end of file