diff --git a/packages/@ember/-internals/routing/lib/system/router.ts b/packages/@ember/-internals/routing/lib/system/router.ts
index a31c86797cd..8d308366159 100644
--- a/packages/@ember/-internals/routing/lib/system/router.ts
+++ b/packages/@ember/-internals/routing/lib/system/router.ts
@@ -1397,9 +1397,15 @@ function findRouteStateName(route: Route, state: string) {
   return routeHasBeenDefined(owner, router, stateName, stateNameFull) ? stateNameFull : '';
 }
 
+function isPromise(p: any): boolean {
+  return p !== null && typeof p === 'object' && typeof p.then === 'function';
+}
+
 /**
-  Determines whether or not a route has been defined by checking that the route
-  is in the Router's map and the owner has a registration for that route.
+  Determines whether or not a route has been defined by checking
+  - that the route is in the Router's map and
+  - the owner has a registration for that route and
+  - it has been fully resolved (think of aync assets loading)
 
   @private
   @param {Owner} owner
@@ -1410,9 +1416,15 @@ function findRouteStateName(route: Route, state: string) {
 */
 function routeHasBeenDefined(owner: Owner, router: any, localName: string, fullName: string) {
   let routerHasRoute = router.hasRoute(fullName);
-  let ownerHasRoute =
-    owner.hasRegistration(`template:${localName}`) || owner.hasRegistration(`route:${localName}`);
-  return routerHasRoute && ownerHasRoute;
+
+  if (routerHasRoute && !isPromise(router.getRoute(fullName))) {
+    let ownerHasRoute =
+      owner.hasRegistration(`template:${localName}`) || owner.hasRegistration(`route:${localName}`);
+
+    return ownerHasRoute;
+  }
+
+  return false;
 }
 
 export function triggerEvent(
diff --git a/packages/ember/tests/routing/model_loading_test.js b/packages/ember/tests/routing/model_loading_test.js
index 3c614a5b9bb..7911aa00732 100644
--- a/packages/ember/tests/routing/model_loading_test.js
+++ b/packages/ember/tests/routing/model_loading_test.js
@@ -2,11 +2,15 @@
 import { Route } from '@ember/-internals/routing';
 import Controller from '@ember/controller';
 import { Object as EmberObject, A as emberA } from '@ember/-internals/runtime';
-import { moduleFor, ApplicationTestCase, getTextOf } from 'internal-test-helpers';
+import {
+  moduleFor,
+  ApplicationTestCase,
+  getTextOf,
+  runLoopSettled,
+  lazyLoadingRouterOptions,
+} from 'internal-test-helpers';
 import { run } from '@ember/runloop';
 import { computed, set } from '@ember/-internals/metal';
-import { isDestroying } from '@glimmer/destroyable';
-import RSVP from 'rsvp';
 
 let originalConsoleError;
 
@@ -502,8 +506,6 @@ class LoadingTests extends ApplicationTestCase {
       })
     );
 
-    this.add('route:loading', Route.extend({}));
-    this.add('route:home', Route.extend({}));
     this.add(
       'route:special',
       Route.extend({
@@ -518,7 +520,6 @@ class LoadingTests extends ApplicationTestCase {
 
     this.addTemplate('root.index', '
Home
');
     this.addTemplate('special', '{{@model.id}}
');
-    this.addTemplate('loading', 'LOADING!
');
 
     return this.visit('/').then(() => {
       rootElement = document.getElementById('qunit-fixture');
@@ -755,7 +756,7 @@ class LoadingTests extends ApplicationTestCase {
       });
   }
 
-  ['@test Parent route context change'](assert) {
+  async ['@test Parent route context change'](assert) {
     let editCount = 0;
     let editedPostIds = emberA();
 
@@ -777,8 +778,8 @@ class LoadingTests extends ApplicationTestCase {
       'route:posts',
       Route.extend({
         actions: {
-          showPost(context) {
-            expectDeprecation(() => {
+          async showPost(context) {
+            await expectDeprecationAsync(() => {
               this.transitionTo('post', context);
             }, /Calling transitionTo on a route is deprecated/);
           },
@@ -798,8 +799,8 @@ class LoadingTests extends ApplicationTestCase {
         },
 
         actions: {
-          editPost() {
-            expectDeprecation(() => {
+          async editPost() {
+            await expectDeprecationAsync(() => {
               this.transitionTo('post.edit');
             }, /Calling transitionTo on a route is deprecated/);
           },
@@ -815,6 +816,7 @@ class LoadingTests extends ApplicationTestCase {
           editedPostIds.push(postId);
           return null;
         },
+
         setup() {
           this._super(...arguments);
           editCount++;
@@ -822,15 +824,18 @@ class LoadingTests extends ApplicationTestCase {
       })
     );
 
-    return this.visit('/posts/1').then(() => {
-      assert.ok(true, '/posts/1 has been handled');
-      let router = this.applicationInstance.lookup('router:main');
-      run(() => router.send('editPost'));
-      run(() => router.send('showPost', { id: '2' }));
-      run(() => router.send('editPost'));
-      assert.equal(editCount, 2, 'set up the edit route twice without failure');
-      assert.deepEqual(editedPostIds, ['1', '2'], 'modelFor posts.post returns the right context');
-    });
+    await this.visit('/posts/1');
+    assert.ok(true, '/posts/1 has been handled');
+    let router = this.applicationInstance.lookup('router:main');
+    run(() => router.send('editPost'));
+    await runLoopSettled();
+    await runLoopSettled();
+    run(() => router.send('showPost', { id: '2' }));
+    await runLoopSettled();
+    run(() => router.send('editPost'));
+    await runLoopSettled();
+    assert.equal(editCount, 2, 'set up the edit route twice without failure');
+    assert.deepEqual(editedPostIds, ['1', '2'], 'modelFor posts.post returns the right context');
   }
 
   ['@test ApplicationRoute with model does not proxy the currentPath'](assert) {
@@ -941,6 +946,14 @@ class LoadingTests extends ApplicationTestCase {
         assert.equal(childcount, 2);
       });
   }
+
+  ['@test What about loading states'](assert) {
+    assert.expect(1);
+    this.add('route:loading', Route.extend({}));
+    return this.visit('/').then(() => {
+      assert.ok(true);
+    });
+  }
 }
 
 moduleFor('Route - model loading', LoadingTests);
@@ -949,43 +962,7 @@ moduleFor(
   'Route - model loading (simulated within lazy engine)',
   class extends LoadingTests {
     get routerOptions() {
-      return {
-        location: 'none',
-        setupRouter() {
-          this._super(...arguments);
-          let getRoute = this._routerMicrolib.getRoute;
-          this._enginePromises = Object.create(null);
-          this._resolvedEngines = Object.create(null);
-
-          let routes = new Map();
-          let routePromises = new Map();
-          this._routerMicrolib.getRoute = (name) => {
-            if (routes.has(name)) {
-              return routes.get(name);
-            }
-
-            if (routePromises.has(name)) {
-              return routePromises.get(name);
-            }
-
-            let promise = new RSVP.Promise((resolve) => {
-              setTimeout(() => {
-                if (isDestroying(this)) {
-                  return;
-                }
-
-                let route = getRoute(name);
-
-                routes.set(name, route);
-                resolve(route);
-              }, 10);
-            });
-            routePromises.set(name, promise);
-
-            return promise;
-          };
-        },
-      };
+      return lazyLoadingRouterOptions;
     }
   }
 );
diff --git a/packages/ember/tests/routing/substates_test.js b/packages/ember/tests/routing/substates_test.js
index 13330e7d0c1..bfa3a8c457c 100644
--- a/packages/ember/tests/routing/substates_test.js
+++ b/packages/ember/tests/routing/substates_test.js
@@ -2,7 +2,12 @@ import { RSVP } from '@ember/-internals/runtime';
 import { Route } from '@ember/-internals/routing';
 import Controller from '@ember/controller';
 
-import { moduleFor, ApplicationTestCase, runTask } from 'internal-test-helpers';
+import {
+  moduleFor,
+  ApplicationTestCase,
+  runTask,
+  lazyLoadingRouterOptions,
+} from 'internal-test-helpers';
 
 let counter;
 
@@ -10,1275 +15,1280 @@ function step(assert, expectedValue, description) {
   assert.equal(counter, expectedValue, 'Step ' + expectedValue + ': ' + description);
   counter++;
 }
+class LoadingErrorSubstatesTests extends ApplicationTestCase {
+  constructor() {
+    super(...arguments);
+    counter = 1;
 
-moduleFor(
-  'Loading/Error Substates',
-  class extends ApplicationTestCase {
-    constructor() {
-      super(...arguments);
-      counter = 1;
-
-      this.addTemplate('application', `{{outlet}}
`);
-      this.addTemplate('index', 'INDEX');
-    }
+    this.addTemplate('application', `{{outlet}}
`);
+    this.addTemplate('index', 'INDEX');
+  }
 
-    visit(...args) {
-      return runTask(() => super.visit(...args));
-    }
+  visit(...args) {
+    return runTask(() => super.visit(...args));
+  }
 
-    getController(name) {
-      return this.applicationInstance.lookup(`controller:${name}`);
-    }
+  getController(name) {
+    return this.applicationInstance.lookup(`controller:${name}`);
+  }
 
-    get currentPath() {
-      let currentPath;
-      expectDeprecation(() => {
-        currentPath = this.getController('application').get('currentPath');
-      }, 'Accessing `currentPath` on `controller:application` is deprecated, use the `currentPath` property on `service:router` instead.');
-      return currentPath;
-    }
+  get currentPath() {
+    let currentPath;
+    expectDeprecation(() => {
+      currentPath = this.getController('application').get('currentPath');
+    }, 'Accessing `currentPath` on `controller:application` is deprecated, use the `currentPath` property on `service:router` instead.');
+    return currentPath;
+  }
 
-    ['@test Slow promise from a child route of application enters nested loading state'](assert) {
-      let turtleDeferred = RSVP.defer();
+  ['@test Slow promise from a child route of application enters nested loading state'](assert) {
+    let turtleDeferred = RSVP.defer();
 
-      this.router.map(function () {
-        this.route('turtle');
-      });
+    this.router.map(function () {
+      this.route('turtle');
+    });
 
-      this.add(
-        'route:application',
-        Route.extend({
-          setupController() {
-            step(assert, 2, 'ApplicationRoute#setupController');
-          },
-        })
-      );
+    this.add(
+      'route:application',
+      Route.extend({
+        setupController() {
+          step(assert, 2, 'ApplicationRoute#setupController');
+        },
+      })
+    );
+
+    this.add(
+      'route:turtle',
+      Route.extend({
+        model() {
+          step(assert, 1, 'TurtleRoute#model');
+          return turtleDeferred.promise;
+        },
+      })
+    );
+    this.addTemplate('turtle', 'TURTLE');
+    this.addTemplate('loading', 'LOADING');
 
-      this.add(
-        'route:turtle',
-        Route.extend({
-          model() {
-            step(assert, 1, 'TurtleRoute#model');
-            return turtleDeferred.promise;
-          },
-        })
-      );
-      this.addTemplate('turtle', 'TURTLE');
-      this.addTemplate('loading', 'LOADING');
+    let promise = this.visit('/turtle').then(() => {
+      text = this.$('#app').text();
+      assert.equal(text, 'TURTLE', `turtle template has loaded and replaced the loading template`);
+    });
+
+    let text = this.$('#app').text();
+    assert.equal(
+      text,
+      'LOADING',
+      `The Loading template is nested in application template's outlet`
+    );
+
+    turtleDeferred.resolve();
+    return promise;
+  }
 
-      let promise = this.visit('/turtle').then(() => {
-        text = this.$('#app').text();
-        assert.equal(
-          text,
-          'TURTLE',
-          `turtle template has loaded and replaced the loading template`
-        );
-      });
+  [`@test Slow promises returned from ApplicationRoute#model don't enter LoadingRoute`](assert) {
+    let appDeferred = RSVP.defer();
+
+    this.add(
+      'route:application',
+      Route.extend({
+        model() {
+          return appDeferred.promise;
+        },
+      })
+    );
+    this.add(
+      'route:loading',
+      Route.extend({
+        setupController() {
+          assert.ok(false, `shouldn't get here`);
+        },
+      })
+    );
 
+    let promise = this.visit('/').then(() => {
       let text = this.$('#app').text();
-      assert.equal(
-        text,
-        'LOADING',
-        `The Loading template is nested in application template's outlet`
-      );
 
-      turtleDeferred.resolve();
-      return promise;
+      assert.equal(text, 'INDEX', `index template has been rendered`);
+    });
+
+    if (this.element) {
+      assert.equal(this.element.textContent, '');
     }
 
-    [`@test Slow promises returned from ApplicationRoute#model don't enter LoadingRoute`](assert) {
-      let appDeferred = RSVP.defer();
+    appDeferred.resolve();
 
-      this.add(
-        'route:application',
-        Route.extend({
-          model() {
-            return appDeferred.promise;
-          },
-        })
-      );
-      this.add(
-        'route:loading',
-        Route.extend({
-          setupController() {
-            assert.ok(false, `shouldn't get here`);
-          },
-        })
-      );
+    return promise;
+  }
+
+  [`@test Don't enter loading route unless either route or template defined`](assert) {
+    let deferred = RSVP.defer();
+
+    this.router.map(function () {
+      this.route('dummy');
+    });
+    this.add(
+      'route:dummy',
+      Route.extend({
+        model() {
+          return deferred.promise;
+        },
+      })
+    );
+    this.addTemplate('dummy', 'DUMMY');
 
-      let promise = this.visit('/').then(() => {
+    return this.visit('/').then(() => {
+      let promise = this.visit('/dummy').then(() => {
         let text = this.$('#app').text();
 
-        assert.equal(text, 'INDEX', `index template has been rendered`);
+        assert.equal(text, 'DUMMY', `dummy template has been rendered`);
       });
 
-      if (this.element) {
-        assert.equal(this.element.textContent, '');
-      }
-
-      appDeferred.resolve();
-
-      return promise;
-    }
-
-    [`@test Don't enter loading route unless either route or template defined`](assert) {
-      let deferred = RSVP.defer();
-
-      this.router.map(function () {
-        this.route('dummy');
-      });
-      this.add(
-        'route:dummy',
-        Route.extend({
-          model() {
-            return deferred.promise;
-          },
-        })
+      assert.ok(
+        this.currentPath !== 'loading',
+        `
+      loading state not entered
+    `
       );
-      this.addTemplate('dummy', 'DUMMY');
+      deferred.resolve();
 
-      return this.visit('/').then(() => {
-        let promise = this.visit('/dummy').then(() => {
-          let text = this.$('#app').text();
+      return promise;
+    });
+  }
 
-          assert.equal(text, 'DUMMY', `dummy template has been rendered`);
-        });
+  ['@test Enter loading route only if loadingRoute is defined'](assert) {
+    let deferred = RSVP.defer();
 
-        assert.ok(
-          this.currentPath !== 'loading',
-          `
-        loading state not entered
-      `
-        );
-        deferred.resolve();
+    this.router.map(function () {
+      this.route('dummy');
+    });
 
-        return promise;
-      });
-    }
+    this.add(
+      'route:dummy',
+      Route.extend({
+        model() {
+          step(assert, 1, 'DummyRoute#model');
+          return deferred.promise;
+        },
+      })
+    );
+    this.add(
+      'route:loading',
+      Route.extend({
+        setupController() {
+          step(assert, 2, 'LoadingRoute#setupController');
+        },
+      })
+    );
+    this.addTemplate('dummy', 'DUMMY');
 
-    ['@test Enter loading route only if loadingRoute is defined'](assert) {
-      let deferred = RSVP.defer();
+    return this.visit('/').then(() => {
+      let promise = this.visit('/dummy').then(() => {
+        let text = this.$('#app').text();
 
-      this.router.map(function () {
-        this.route('dummy');
+        assert.equal(text, 'DUMMY', `dummy template has been rendered`);
       });
+      assert.equal(this.currentPath, 'loading', `loading state entered`);
+      deferred.resolve();
 
-      this.add(
-        'route:dummy',
-        Route.extend({
-          model() {
-            step(assert, 1, 'DummyRoute#model');
-            return deferred.promise;
-          },
-        })
-      );
-      this.add(
-        'route:loading',
-        Route.extend({
-          setupController() {
-            step(assert, 2, 'LoadingRoute#setupController');
-          },
-        })
-      );
-      this.addTemplate('dummy', 'DUMMY');
+      return promise;
+    });
+  }
 
-      return this.visit('/').then(() => {
-        let promise = this.visit('/dummy').then(() => {
-          let text = this.$('#app').text();
+  ['@test Enter loading route with correct query parameters'](assert) {
+    let deferred = RSVP.defer();
 
-          assert.equal(text, 'DUMMY', `dummy template has been rendered`);
-        });
-        assert.equal(this.currentPath, 'loading', `loading state entered`);
-        deferred.resolve();
+    this.router.map(function () {
+      this.route('dummy');
+    });
 
-        return promise;
-      });
-    }
-
-    ['@test Enter loading route with correct query parameters'](assert) {
-      let deferred = RSVP.defer();
+    this.add(
+      'route:dummy',
+      Route.extend({
+        model() {
+          step(assert, 1, 'DummyRoute#model');
+          return deferred.promise;
+        },
+      })
+    );
 
-      this.router.map(function () {
-        this.route('dummy');
-      });
+    this.add(
+      'controller:application',
+      class extends Controller {
+        queryParams = ['qux'];
 
-      this.add(
-        'route:dummy',
-        Route.extend({
-          model() {
-            step(assert, 1, 'DummyRoute#model');
-            return deferred.promise;
-          },
-        })
-      );
+        qux = 'initial';
+      }
+    );
 
-      this.add(
-        'controller:application',
-        class extends Controller {
-          queryParams = ['qux'];
+    this.add(
+      'route:loading',
+      Route.extend({
+        setupController() {
+          step(assert, 2, 'LoadingRoute#setupController');
+        },
+      })
+    );
+    this.addTemplate('dummy', 'DUMMY');
 
-          qux = 'initial';
-        }
+    return this.visit('/?qux=updated').then(() => {
+      assert.equal(
+        this.getController('application').qux,
+        'updated',
+        'the application controller has the correct qp value'
       );
 
-      this.add(
-        'route:loading',
-        Route.extend({
-          setupController() {
-            step(assert, 2, 'LoadingRoute#setupController');
-          },
-        })
-      );
-      this.addTemplate('dummy', 'DUMMY');
+      let promise = this.visit('/dummy?qux=updated').then(() => {
+        let text = this.$('#app').text();
 
-      return this.visit('/?qux=updated').then(() => {
+        assert.equal(text, 'DUMMY', `dummy template has been rendered`);
         assert.equal(
           this.getController('application').qux,
           'updated',
           'the application controller has the correct qp value'
         );
+      });
 
-        let promise = this.visit('/dummy?qux=updated').then(() => {
-          let text = this.$('#app').text();
+      assert.equal(this.currentPath, 'loading', `loading state entered`);
+      assert.equal(
+        this.currentURL,
+        '/dummy?qux=updated',
+        `during loading url reflect the correct state`
+      );
+      assert.equal(
+        this.getController('application').qux,
+        'updated',
+        'the application controller has the correct qp value'
+      );
 
-          assert.equal(text, 'DUMMY', `dummy template has been rendered`);
-          assert.equal(
-            this.getController('application').qux,
-            'updated',
-            'the application controller has the correct qp value'
-          );
-        });
+      deferred.resolve();
 
-        assert.equal(this.currentPath, 'loading', `loading state entered`);
-        assert.equal(
-          this.currentURL,
-          '/dummy?qux=updated',
-          `during loading url reflect the correct state`
-        );
-        assert.equal(
-          this.getController('application').qux,
-          'updated',
-          'the application controller has the correct qp value'
-        );
+      return promise;
+    });
+  }
 
-        deferred.resolve();
+  ['@test Enter child-loading route with correct query parameters'](assert) {
+    assert.expect(9);
+    let deferred = RSVP.defer();
 
-        return promise;
+    this.router.map(function () {
+      this.route('parent', function () {
+        this.route('child');
       });
-    }
+    });
+
+    this.add(
+      'route:parent.child',
+      Route.extend({
+        model() {
+          step(assert, 1, 'ChildRoute#model');
+          return deferred.promise;
+        },
+      })
+    );
 
-    ['@test Enter child-loading route with correct query parameters'](assert) {
-      assert.expect(9);
-      let deferred = RSVP.defer();
+    this.add(
+      'controller:parent',
+      class extends Controller {
+        queryParams = ['qux'];
 
-      this.router.map(function () {
-        this.route('parent', function () {
-          this.route('child');
-        });
-      });
-
-      this.add(
-        'route:parent.child',
-        Route.extend({
-          model() {
-            step(assert, 1, 'ChildRoute#model');
-            return deferred.promise;
-          },
-        })
-      );
+        qux = 'initial';
+      }
+    );
 
-      this.add(
-        'controller:parent',
-        class extends Controller {
-          queryParams = ['qux'];
+    this.add(
+      'route:parent.child_loading',
+      Route.extend({
+        setupController() {
+          step(assert, 2, 'ChildLoadingRoute#setupController');
+        },
+      })
+    );
+    this.addTemplate('parent', 'PARENT {{outlet}}');
 
-          qux = 'initial';
-        }
-      );
+    this.addTemplate('parent.child', 'CHILD');
 
-      this.add(
-        'route:parent.child_loading',
-        Route.extend({
-          setupController() {
-            step(assert, 2, 'ChildLoadingRoute#setupController');
-          },
-        })
+    return this.visit('/parent?qux=updated').then(() => {
+      assert.equal(
+        this.getController('parent').qux,
+        'updated',
+        'in the parent route, the parent controller has the correct qp value'
       );
-      this.addTemplate('parent', 'PARENT {{outlet}}');
 
-      this.addTemplate('parent.child', 'CHILD');
-
-      return this.visit('/parent?qux=updated').then(() => {
-        assert.equal(
-          this.getController('parent').qux,
-          'updated',
-          'in the parent route, the parent controller has the correct qp value'
-        );
-
-        let promise = this.visit('/parent/child?qux=updated').then(() => {
-          let text = this.$('#app').text();
-
-          assert.equal(text, 'PARENT CHILD', `child template has been rendered`);
-          assert.equal(
-            this.getController('parent').qux,
-            'updated',
-            'after entered in the parent.child route, the parent controller has the correct qp value'
-          );
-        });
+      let promise = this.visit('/parent/child?qux=updated').then(() => {
+        let text = this.$('#app').text();
 
-        assert.equal(this.currentPath, 'parent.child_loading', `child loading state entered`);
-        assert.equal(
-          this.currentURL,
-          '/parent/child?qux=updated',
-          `during child loading, url reflect the correct state`
-        );
+        assert.equal(text, 'PARENT CHILD', `child template has been rendered`);
         assert.equal(
           this.getController('parent').qux,
           'updated',
-          'in the child_loading route, the parent controller has the correct qp value'
+          'after entered in the parent.child route, the parent controller has the correct qp value'
         );
-
-        deferred.resolve();
-
-        return promise;
       });
-    }
 
-    ['@test Slow promises returned from ApplicationRoute#model enter ApplicationLoadingRoute if present'](
-      assert
-    ) {
-      let appDeferred = RSVP.defer();
-
-      this.add(
-        'route:application',
-        Route.extend({
-          model() {
-            return appDeferred.promise;
-          },
-        })
+      assert.equal(this.currentPath, 'parent.child_loading', `child loading state entered`);
+      assert.equal(
+        this.currentURL,
+        '/parent/child?qux=updated',
+        `during child loading, url reflect the correct state`
       );
-      let loadingRouteEntered = false;
-      this.add(
-        'route:application_loading',
-        Route.extend({
-          setupController() {
-            loadingRouteEntered = true;
-          },
-        })
+      assert.equal(
+        this.getController('parent').qux,
+        'updated',
+        'in the child_loading route, the parent controller has the correct qp value'
       );
 
-      let promise = this.visit('/').then(() => {
-        assert.equal(this.$('#app').text(), 'INDEX', 'index route loaded');
-      });
-      assert.ok(loadingRouteEntered, 'ApplicationLoadingRoute was entered');
-      appDeferred.resolve();
+      deferred.resolve();
 
       return promise;
-    }
-
-    ['@test Slow promises returned from ApplicationRoute#model enter application_loading if template present'](
-      assert
-    ) {
-      let appDeferred = RSVP.defer();
+    });
+  }
 
-      this.addTemplate(
-        'application_loading',
-        `
-      TOPLEVEL LOADING
-    `
-      );
-      this.add(
-        'route:application',
-        Route.extend({
-          model() {
-            return appDeferred.promise;
-          },
-        })
-      );
+  ['@test Slow promises returned from ApplicationRoute#model enter ApplicationLoadingRoute if present'](
+    assert
+  ) {
+    let appDeferred = RSVP.defer();
 
-      let promise = this.visit('/').then(() => {
-        let length = this.$('#toplevel-loading').length;
-        text = this.$('#app').text();
+    this.add(
+      'route:application',
+      Route.extend({
+        model() {
+          return appDeferred.promise;
+        },
+      })
+    );
+    let loadingRouteEntered = false;
+    this.add(
+      'route:application_loading',
+      Route.extend({
+        setupController() {
+          loadingRouteEntered = true;
+        },
+      })
+    );
 
-        assert.equal(length, 0, `top-level loading view has been entirely removed from the DOM`);
-        assert.equal(text, 'INDEX', 'index has fully rendered');
-      });
-      let text = this.$('#toplevel-loading').text();
+    let promise = this.visit('/').then(() => {
+      assert.equal(this.$('#app').text(), 'INDEX', 'index route loaded');
+    });
+    assert.ok(loadingRouteEntered, 'ApplicationLoadingRoute was entered');
+    appDeferred.resolve();
 
-      assert.equal(text, 'TOPLEVEL LOADING', 'still loading the top level');
-      appDeferred.resolve();
+    return promise;
+  }
 
-      return promise;
-    }
+  ['@test Slow promises returned from ApplicationRoute#model enter application_loading if template present'](
+    assert
+  ) {
+    let appDeferred = RSVP.defer();
 
-    ['@test Prioritized substate entry works with preserved-namespace nested routes'](assert) {
-      let deferred = RSVP.defer();
+    this.addTemplate(
+      'application_loading',
+      `
+    TOPLEVEL LOADING
+  `
+    );
+    this.add(
+      'route:application',
+      Route.extend({
+        model() {
+          return appDeferred.promise;
+        },
+      })
+    );
 
-      this.addTemplate('foo.bar_loading', 'FOOBAR LOADING');
-      this.addTemplate('foo.bar.index', 'YAY');
+    let promise = this.visit('/').then(() => {
+      let length = this.$('#toplevel-loading').length;
+      text = this.$('#app').text();
 
-      this.router.map(function () {
-        this.route('foo', function () {
-          this.route('bar', { path: '/bar' }, function () {});
-        });
-      });
+      assert.equal(length, 0, `top-level loading view has been entirely removed from the DOM`);
+      assert.equal(text, 'INDEX', 'index has fully rendered');
+    });
+    let text = this.$('#toplevel-loading').text();
 
-      this.add(
-        'route:foo.bar',
-        Route.extend({
-          model() {
-            return deferred.promise;
-          },
-        })
-      );
+    assert.equal(text, 'TOPLEVEL LOADING', 'still loading the top level');
+    appDeferred.resolve();
 
-      return this.visit('/').then(() => {
-        let promise = this.visit('/foo/bar').then(() => {
-          text = this.$('#app').text();
+    return promise;
+  }
 
-          assert.equal(text, 'YAY', 'foo.bar.index fully loaded');
-        });
-        let text = this.$('#app').text();
+  ['@test Prioritized substate entry works with preserved-namespace nested routes'](assert) {
+    let deferred = RSVP.defer();
 
-        assert.equal(
-          text,
-          'FOOBAR LOADING',
-          `foo.bar_loading was entered (as opposed to something like foo/foo/bar_loading)`
-        );
-        deferred.resolve();
+    this.addTemplate('foo.bar_loading', 'FOOBAR LOADING');
+    this.addTemplate('foo.bar.index', 'YAY');
 
-        return promise;
+    this.router.map(function () {
+      this.route('foo', function () {
+        this.route('bar', { path: '/bar' }, function () {});
       });
-    }
+    });
 
-    ['@test Prioritized substate entry works with reset-namespace nested routes'](assert) {
-      let deferred = RSVP.defer();
+    this.add(
+      'route:foo.bar',
+      Route.extend({
+        model() {
+          return deferred.promise;
+        },
+      })
+    );
 
-      this.addTemplate('bar_loading', 'BAR LOADING');
-      this.addTemplate('bar.index', 'YAY');
+    return this.visit('/').then(() => {
+      let promise = this.visit('/foo/bar').then(() => {
+        text = this.$('#app').text();
 
-      this.router.map(function () {
-        this.route('foo', function () {
-          this.route('bar', { path: '/bar', resetNamespace: true }, function () {});
-        });
+        assert.equal(text, 'YAY', 'foo.bar.index fully loaded');
       });
+      let text = this.$('#app').text();
 
-      this.add(
-        'route:bar',
-        Route.extend({
-          model() {
-            return deferred.promise;
-          },
-        })
+      assert.equal(
+        text,
+        'FOOBAR LOADING',
+        `foo.bar_loading was entered (as opposed to something like foo/foo/bar_loading)`
       );
+      deferred.resolve();
 
-      return this.visit('/').then(() => {
-        let promise = this.visit('/foo/bar').then(() => {
-          text = this.$('#app').text();
-
-          assert.equal(text, 'YAY', 'bar.index fully loaded');
-        });
-
-        let text = this.$('#app').text();
-
-        assert.equal(
-          text,
-          'BAR LOADING',
-          `foo.bar_loading was entered (as opposed to something likefoo/foo/bar_loading)`
-        );
-        deferred.resolve();
-
-        return promise;
-      });
-    }
+      return promise;
+    });
+  }
 
-    ['@test Prioritized loading substate entry works with preserved-namespace nested routes'](
-      assert
-    ) {
-      let deferred = RSVP.defer();
+  ['@test Prioritized substate entry works with reset-namespace nested routes'](assert) {
+    let deferred = RSVP.defer();
 
-      this.addTemplate('foo.bar_loading', 'FOOBAR LOADING');
-      this.addTemplate('foo.bar', 'YAY');
+    this.addTemplate('bar_loading', 'BAR LOADING');
+    this.addTemplate('bar.index', 'YAY');
 
-      this.router.map(function () {
-        this.route('foo', function () {
-          this.route('bar');
-        });
+    this.router.map(function () {
+      this.route('foo', function () {
+        this.route('bar', { path: '/bar', resetNamespace: true }, function () {});
       });
+    });
 
-      this.add(
-        'route:foo.bar',
-        Route.extend({
-          model() {
-            return deferred.promise;
-          },
-        })
-      );
+    this.add(
+      'route:bar',
+      Route.extend({
+        model() {
+          return deferred.promise;
+        },
+      })
+    );
 
+    return this.visit('/').then(() => {
       let promise = this.visit('/foo/bar').then(() => {
         text = this.$('#app').text();
 
-        assert.equal(text, 'YAY', 'foo.bar has rendered');
+        assert.equal(text, 'YAY', 'bar.index fully loaded');
       });
+
       let text = this.$('#app').text();
 
       assert.equal(
         text,
-        'FOOBAR LOADING',
-        `foo.bar_loading was entered (as opposed to something like foo/foo/bar_loading)`
+        'BAR LOADING',
+        `foo.bar_loading was entered (as opposed to something likefoo/foo/bar_loading)`
       );
       deferred.resolve();
 
       return promise;
-    }
+    });
+  }
 
-    async ['@test Prioritized error substate entry works with preserved-namespace nested routes'](
-      assert
-    ) {
-      this.addTemplate('foo.bar_error', 'FOOBAR ERROR: {{@model.msg}}');
-      this.addTemplate('foo.bar', 'YAY');
+  ['@test Prioritized loading substate entry works with preserved-namespace nested routes'](
+    assert
+  ) {
+    let deferred = RSVP.defer();
 
-      this.router.map(function () {
-        this.route('foo', function () {
-          this.route('bar');
-        });
+    this.addTemplate('foo.bar_loading', 'FOOBAR LOADING');
+    this.addTemplate('foo.bar', 'YAY');
+
+    this.router.map(function () {
+      this.route('foo', function () {
+        this.route('bar');
       });
+    });
 
-      this.add(
-        'route:foo.bar',
-        Route.extend({
-          model() {
-            return RSVP.reject({
-              msg: 'did it broke?',
-            });
-          },
-        })
-      );
+    this.add(
+      'route:foo.bar',
+      Route.extend({
+        model() {
+          return deferred.promise;
+        },
+      })
+    );
 
-      await this.visit('/');
+    let promise = this.visit('/foo/bar').then(() => {
+      text = this.$('#app').text();
 
-      await this.visit('/foo/bar');
+      assert.equal(text, 'YAY', 'foo.bar has rendered');
+    });
+    let text = this.$('#app').text();
 
-      assert.equal(
-        this.$('#app').text(),
-        'FOOBAR ERROR: did it broke?',
-        `foo.bar_error was entered (as opposed to something like foo/foo/bar_error)`
-      );
-    }
+    assert.equal(
+      text,
+      'FOOBAR LOADING',
+      `foo.bar_loading was entered (as opposed to something like foo/foo/bar_loading)`
+    );
+    deferred.resolve();
 
-    ['@test Prioritized loading substate entry works with auto-generated index routes'](assert) {
-      let deferred = RSVP.defer();
-      this.addTemplate('foo.index_loading', 'FOO LOADING');
-      this.addTemplate('foo.index', 'YAY');
-      this.addTemplate('foo', '{{outlet}}');
+    return promise;
+  }
 
-      this.router.map(function () {
-        this.route('foo', function () {
-          this.route('bar');
-        });
+  async ['@test Prioritized error substate entry works with preserved-namespace nested routes'](
+    assert
+  ) {
+    this.addTemplate('foo.bar_error', 'FOOBAR ERROR: {{@model.msg}}');
+    this.addTemplate('foo.bar', 'YAY');
+
+    this.router.map(function () {
+      this.route('foo', function () {
+        this.route('bar');
       });
+    });
+
+    this.add(
+      'route:foo.bar',
+      Route.extend({
+        model() {
+          return RSVP.reject({
+            msg: 'did it broke?',
+          });
+        },
+      })
+    );
 
-      this.add(
-        'route:foo.index',
-        Route.extend({
-          model() {
-            return deferred.promise;
-          },
-        })
-      );
-      this.add(
-        'route:foo',
-        Route.extend({
-          model() {
-            return true;
-          },
-        })
-      );
+    await this.visit('/');
 
-      let promise = this.visit('/foo').then(() => {
-        text = this.$('#app').text();
+    await this.visit('/foo/bar');
 
-        assert.equal(text, 'YAY', 'foo.index was rendered');
+    assert.equal(
+      this.$('#app').text(),
+      'FOOBAR ERROR: did it broke?',
+      `foo.bar_error was entered (as opposed to something like foo/foo/bar_error)`
+    );
+  }
+
+  ['@test Prioritized loading substate entry works with auto-generated index routes'](assert) {
+    let deferred = RSVP.defer();
+    this.addTemplate('foo.index_loading', 'FOO LOADING');
+    this.addTemplate('foo.index', 'YAY');
+    this.addTemplate('foo', '{{outlet}}');
+
+    this.router.map(function () {
+      this.route('foo', function () {
+        this.route('bar');
       });
-      let text = this.$('#app').text();
-      assert.equal(text, 'FOO LOADING', 'foo.index_loading was entered');
+    });
 
-      deferred.resolve();
+    this.add(
+      'route:foo.index',
+      Route.extend({
+        model() {
+          return deferred.promise;
+        },
+      })
+    );
+    this.add(
+      'route:foo',
+      Route.extend({
+        model() {
+          return true;
+        },
+      })
+    );
 
-      return promise;
-    }
+    let promise = this.visit('/foo').then(() => {
+      text = this.$('#app').text();
 
-    async ['@test Prioritized error substate entry works with auto-generated index routes'](
-      assert
-    ) {
-      this.addTemplate('foo.index_error', 'FOO ERROR: {{@model.msg}}');
-      this.addTemplate('foo.index', 'YAY');
-      this.addTemplate('foo', '{{outlet}}');
+      assert.equal(text, 'YAY', 'foo.index was rendered');
+    });
+    let text = this.$('#app').text();
+    assert.equal(text, 'FOO LOADING', 'foo.index_loading was entered');
 
-      this.router.map(function () {
-        this.route('foo', function () {
-          this.route('bar');
-        });
-      });
+    deferred.resolve();
 
-      this.add(
-        'route:foo.index',
-        Route.extend({
-          model() {
-            return RSVP.reject({
-              msg: 'did it broke?',
-            });
-          },
-        })
-      );
-      this.add(
-        'route:foo',
-        Route.extend({
-          model() {
-            return true;
-          },
-        })
-      );
+    return promise;
+  }
 
-      await this.visit('/');
+  async ['@test Prioritized error substate entry works with auto-generated index routes'](assert) {
+    this.addTemplate('foo.index_error', 'FOO ERROR: {{@model.msg}}');
+    this.addTemplate('foo.index', 'YAY');
+    this.addTemplate('foo', '{{outlet}}');
 
-      await this.visit('/foo');
+    this.router.map(function () {
+      this.route('foo', function () {
+        this.route('bar');
+      });
+    });
+
+    this.add(
+      'route:foo.index',
+      Route.extend({
+        model() {
+          return RSVP.reject({
+            msg: 'did it broke?',
+          });
+        },
+      })
+    );
+    this.add(
+      'route:foo',
+      Route.extend({
+        model() {
+          return true;
+        },
+      })
+    );
 
-      assert.equal(
-        this.$('#app').text(),
-        'FOO ERROR: did it broke?',
-        'foo.index_error was entered'
-      );
-    }
+    await this.visit('/');
 
-    async ['@test Rejected promises returned from ApplicationRoute transition into top-level application_error'](
-      assert
-    ) {
-      let reject = true;
-
-      this.addTemplate('index', 'INDEX
');
-      this.add(
-        'route:application',
-        Route.extend({
-          init() {
-            this._super(...arguments);
-          },
-          model() {
-            if (reject) {
-              return RSVP.reject({ msg: 'BAD NEWS BEARS' });
-            } else {
-              return {};
-            }
-          },
-        })
-      );
+    await this.visit('/foo');
 
-      this.addTemplate(
-        'application_error',
-        `TOPLEVEL ERROR: {{@model.msg}}
`
-      );
+    assert.equal(this.$('#app').text(), 'FOO ERROR: did it broke?', 'foo.index_error was entered');
+  }
 
-      await this.visit('/');
+  async ['@test Rejected promises returned from ApplicationRoute transition into top-level application_error'](
+    assert
+  ) {
+    let reject = true;
+
+    this.addTemplate('index', 'INDEX
');
+    this.add(
+      'route:application',
+      Route.extend({
+        init() {
+          this._super(...arguments);
+        },
+        model() {
+          if (reject) {
+            return RSVP.reject({ msg: 'BAD NEWS BEARS' });
+          } else {
+            return {};
+          }
+        },
+      })
+    );
 
-      assert.equal(
-        this.$('#toplevel-error').text(),
-        'TOPLEVEL ERROR: BAD NEWS BEARS',
-        'toplevel error rendered'
-      );
+    this.addTemplate(
+      'application_error',
+      `TOPLEVEL ERROR: {{@model.msg}}
`
+    );
 
-      reject = false;
+    await this.visit('/');
 
-      await this.visit('/');
+    assert.equal(
+      this.$('#toplevel-error').text(),
+      'TOPLEVEL ERROR: BAD NEWS BEARS',
+      'toplevel error rendered'
+    );
 
-      assert.equal(this.$('#index').text(), 'INDEX', 'the index route resolved');
-    }
+    reject = false;
+
+    await this.visit('/');
+
+    assert.equal(this.$('#index').text(), 'INDEX', 'the index route resolved');
   }
-);
+}
 
-moduleFor(
-  'Loading/Error Substates - nested routes',
-  class extends ApplicationTestCase {
-    constructor() {
-      super(...arguments);
-
-      counter = 1;
-
-      this.addTemplate('application', `{{outlet}}
`);
-      this.addTemplate('index', 'INDEX');
-      this.addTemplate('grandma', 'GRANDMA {{outlet}}');
-      this.addTemplate('mom', 'MOM');
-
-      this.router.map(function () {
-        this.route('grandma', function () {
-          this.route('mom', { resetNamespace: true }, function () {
-            this.route('sally');
-            this.route('this-route-throws');
-          });
-          this.route('puppies');
-        });
-        this.route('memere', { path: '/memere/:seg' }, function () {});
-      });
-    }
+class LoadingErrorSubstatesNestedTests extends ApplicationTestCase {
+  constructor() {
+    super(...arguments);
 
-    getController(name) {
-      return this.applicationInstance.lookup(`controller:${name}`);
-    }
+    counter = 1;
 
-    get currentPath() {
-      let currentPath;
-      expectDeprecation(() => {
-        currentPath = this.getController('application').get('currentPath');
-      }, 'Accessing `currentPath` on `controller:application` is deprecated, use the `currentPath` property on `service:router` instead.');
-      return currentPath;
-    }
+    this.addTemplate('application', `{{outlet}}
`);
+    this.addTemplate('index', 'INDEX');
+    this.addTemplate('grandma', 'GRANDMA {{outlet}}');
+    this.addTemplate('mom', 'MOM');
 
-    async ['@test ApplicationRoute#currentPath reflects loading state path'](assert) {
-      await this.visit('/');
+    this.router.map(function () {
+      this.route('grandma', function () {
+        this.route('mom', { resetNamespace: true }, function () {
+          this.route('sally');
+          this.route('this-route-throws');
+        });
+        this.route('puppies');
+      });
+      this.route('memere', { path: '/memere/:seg' }, function () {});
+    });
+  }
 
-      let momDeferred = RSVP.defer();
+  getController(name) {
+    return this.applicationInstance.lookup(`controller:${name}`);
+  }
 
-      this.addTemplate('grandma.loading', 'GRANDMALOADING');
+  get currentPath() {
+    let currentPath;
+    expectDeprecation(() => {
+      currentPath = this.getController('application').get('currentPath');
+    }, 'Accessing `currentPath` on `controller:application` is deprecated, use the `currentPath` property on `service:router` instead.');
+    return currentPath;
+  }
 
-      this.add(
-        'route:mom',
-        Route.extend({
-          model() {
-            return momDeferred.promise;
-          },
-        })
-      );
+  async ['@test ApplicationRoute#currentPath reflects loading state path'](assert) {
+    await this.visit('/');
 
-      let promise = runTask(() => this.visit('/grandma/mom')).then(() => {
-        text = this.$('#app').text();
+    let momDeferred = RSVP.defer();
 
-        assert.equal(text, 'GRANDMA MOM', `Grandma.mom loaded text is displayed`);
-        assert.equal(this.currentPath, 'grandma.mom.index', `currentPath reflects final state`);
-      });
-      let text = this.$('#app').text();
+    this.addTemplate('grandma.loading', 'GRANDMALOADING');
 
-      assert.equal(text, 'GRANDMA GRANDMALOADING', `Grandma.mom loading text displayed`);
+    this.add(
+      'route:mom',
+      Route.extend({
+        model() {
+          return momDeferred.promise;
+        },
+      })
+    );
 
-      assert.equal(this.currentPath, 'grandma.loading', `currentPath reflects loading state`);
+    let promise = runTask(() => this.visit('/grandma/mom')).then(() => {
+      text = this.$('#app').text();
 
-      momDeferred.resolve();
+      assert.equal(text, 'GRANDMA MOM', `Grandma.mom loaded text is displayed`);
+      assert.equal(this.currentPath, 'grandma.mom.index', `currentPath reflects final state`);
+    });
+    let text = this.$('#app').text();
 
-      return promise;
-    }
+    assert.equal(text, 'GRANDMA GRANDMALOADING', `Grandma.mom loading text displayed`);
 
-    async [`@test Loading actions bubble to root but don't enter substates above pivot `](assert) {
-      await this.visit('/');
+    assert.equal(this.currentPath, 'grandma.loading', `currentPath reflects loading state`);
 
-      let sallyDeferred = RSVP.defer();
-      let puppiesDeferred = RSVP.defer();
+    momDeferred.resolve();
 
-      this.add(
-        'route:application',
-        Route.extend({
-          actions: {
-            loading() {
-              assert.ok(true, 'loading action received on ApplicationRoute');
-            },
-          },
-        })
-      );
+    return promise;
+  }
 
-      this.add(
-        'route:mom.sally',
-        Route.extend({
-          model() {
-            return sallyDeferred.promise;
-          },
-        })
-      );
+  async [`@test Loading actions bubble to root but don't enter substates above pivot `](assert) {
+    await this.visit('/');
+
+    let sallyDeferred = RSVP.defer();
+    let puppiesDeferred = RSVP.defer();
 
-      this.add(
-        'route:grandma.puppies',
-        Route.extend({
-          model() {
-            return puppiesDeferred.promise;
+    this.add(
+      'route:application',
+      Route.extend({
+        actions: {
+          loading() {
+            assert.ok(true, 'loading action received on ApplicationRoute');
           },
-        })
-      );
+        },
+      })
+    );
+
+    this.add(
+      'route:mom.sally',
+      Route.extend({
+        model() {
+          return sallyDeferred.promise;
+        },
+      })
+    );
+
+    this.add(
+      'route:grandma.puppies',
+      Route.extend({
+        model() {
+          return puppiesDeferred.promise;
+        },
+      })
+    );
 
-      let promise = this.visit('/grandma/mom/sally');
-      assert.equal(this.currentPath, 'index', 'Initial route fully loaded');
+    let promise = this.visit('/grandma/mom/sally');
+    assert.equal(this.currentPath, 'index', 'Initial route fully loaded');
 
-      sallyDeferred.resolve();
+    sallyDeferred.resolve();
 
-      promise
-        .then(() => {
-          assert.equal(this.currentPath, 'grandma.mom.sally', 'transition completed');
+    promise
+      .then(() => {
+        assert.equal(this.currentPath, 'grandma.mom.sally', 'transition completed');
 
-          let visit = this.visit('/grandma/puppies');
-          assert.equal(
-            this.currentPath,
-            'grandma.mom.sally',
-            'still in initial state because the only loading state is above the pivot route'
-          );
+        let visit = this.visit('/grandma/puppies');
+        assert.equal(
+          this.currentPath,
+          'grandma.mom.sally',
+          'still in initial state because the only loading state is above the pivot route'
+        );
 
-          return visit;
-        })
-        .then(() => {
-          runTask(() => puppiesDeferred.resolve());
+        return visit;
+      })
+      .then(() => {
+        runTask(() => puppiesDeferred.resolve());
 
-          assert.equal(this.currentPath, 'grandma.puppies', 'Finished transition');
-        });
+        assert.equal(this.currentPath, 'grandma.puppies', 'Finished transition');
+      });
 
-      return promise;
-    }
+    return promise;
+  }
 
-    async ['@test Default error event moves into nested route'](assert) {
-      await this.visit('/');
+  async ['@test Default error event moves into nested route'](assert) {
+    await this.visit('/');
 
-      this.addTemplate('grandma.error', 'ERROR: {{@model.msg}}');
+    this.addTemplate('grandma.error', 'ERROR: {{@model.msg}}');
 
-      this.add(
-        'route:mom.sally',
-        Route.extend({
-          model() {
-            step(assert, 1, 'MomSallyRoute#model');
-            return RSVP.reject({
-              msg: 'did it broke?',
-            });
-          },
-          actions: {
-            error() {
-              step(assert, 2, 'MomSallyRoute#actions.error');
-              return true;
-            },
+    this.add(
+      'route:mom.sally',
+      Route.extend({
+        model() {
+          step(assert, 1, 'MomSallyRoute#model');
+          return RSVP.reject({
+            msg: 'did it broke?',
+          });
+        },
+        actions: {
+          error() {
+            step(assert, 2, 'MomSallyRoute#actions.error');
+            return true;
           },
-        })
-      );
+        },
+      })
+    );
 
-      await this.visit('/grandma/mom/sally');
+    await this.visit('/grandma/mom/sally');
 
-      step(assert, 3, 'App finished loading');
+    step(assert, 3, 'App finished loading');
 
-      assert.equal(this.$('#app').text(), 'GRANDMA ERROR: did it broke?', 'error bubbles');
-      assert.equal(this.currentPath, 'grandma.error', 'Initial route fully loaded');
-    }
+    assert.equal(this.$('#app').text(), 'GRANDMA ERROR: did it broke?', 'error bubbles');
+    assert.equal(this.currentPath, 'grandma.error', 'Initial route fully loaded');
+  }
 
-    async [`@test Non-bubbled errors that re-throw aren't swallowed`](assert) {
-      await this.visit('/');
+  async [`@test Non-bubbled errors that re-throw aren't swallowed`](assert) {
+    await this.visit('/');
 
-      this.add(
-        'route:mom.sally',
-        Route.extend({
-          model() {
-            return RSVP.reject({
-              msg: 'did it broke?',
-            });
-          },
-          actions: {
-            error(err) {
-              // returns undefined which is falsey
-              throw err;
-            },
+    this.add(
+      'route:mom.sally',
+      Route.extend({
+        model() {
+          return RSVP.reject({
+            msg: 'did it broke?',
+          });
+        },
+        actions: {
+          error(err) {
+            // returns undefined which is falsey
+            throw err;
           },
-        })
-      );
-
-      await assert.rejects(
-        this.visit('/grandma/mom/sally'),
-        function (err) {
-          return err.msg === 'did it broke?';
         },
-        'it broke'
-      );
-    }
+      })
+    );
+
+    await assert.rejects(
+      this.visit('/grandma/mom/sally'),
+      function (err) {
+        return err.msg === 'did it broke?';
+      },
+      'it broke'
+    );
+  }
 
-    async [`@test Handled errors that re-throw aren't swallowed`](assert) {
-      await this.visit('/');
+  async [`@test Handled errors that re-throw aren't swallowed`](assert) {
+    await this.visit('/');
 
-      let handledError;
+    let handledError;
 
-      this.add(
-        'route:mom.sally',
-        Route.extend({
-          model() {
-            step(assert, 1, 'MomSallyRoute#model');
-            return RSVP.reject({
-              msg: 'did it broke?',
-            });
-          },
-          actions: {
-            error(err) {
-              step(assert, 2, 'MomSallyRoute#actions.error');
-              handledError = err;
-              expectDeprecation(() => {
-                this.transitionTo('mom.this-route-throws');
-              }, /Calling transitionTo on a route is deprecated/);
-
-              return false;
-            },
-          },
-        })
-      );
+    this.add(
+      'route:mom.sally',
+      Route.extend({
+        model() {
+          step(assert, 1, 'MomSallyRoute#model');
+          return RSVP.reject({
+            msg: 'did it broke?',
+          });
+        },
+        actions: {
+          error(err) {
+            step(assert, 2, 'MomSallyRoute#actions.error');
+            handledError = err;
+            expectDeprecation(() => {
+              this.transitionTo('mom.this-route-throws');
+            }, /Calling transitionTo on a route is deprecated/);
 
-      this.add(
-        'route:mom.this-route-throws',
-        Route.extend({
-          model() {
-            step(assert, 3, 'MomThisRouteThrows#model');
-            throw handledError;
+            return false;
           },
-        })
-      );
-
-      await assert.rejects(
-        this.visit('/grandma/mom/sally'),
-        function (err) {
-          return err.msg === 'did it broke?';
         },
-        `it broke`
-      );
-    }
+      })
+    );
+
+    this.add(
+      'route:mom.this-route-throws',
+      Route.extend({
+        model() {
+          step(assert, 3, 'MomThisRouteThrows#model');
+          throw handledError;
+        },
+      })
+    );
+
+    await assert.rejects(
+      this.visit('/grandma/mom/sally'),
+      function (err) {
+        return err.msg === 'did it broke?';
+      },
+      `it broke`
+    );
+  }
 
-    async ['@test errors that are bubbled are thrown at a higher level if not handled'](assert) {
-      await this.visit('/');
-
-      this.add(
-        'route:mom.sally',
-        Route.extend({
-          model() {
-            step(assert, 1, 'MomSallyRoute#model');
-            return RSVP.reject({
-              msg: 'did it broke?',
-            });
-          },
-          actions: {
-            error() {
-              step(assert, 2, 'MomSallyRoute#actions.error');
-              return true;
-            },
-          },
-        })
-      );
+  async ['@test errors that are bubbled are thrown at a higher level if not handled'](assert) {
+    await this.visit('/');
 
-      await assert.rejects(
-        this.visit('/grandma/mom/sally'),
-        function (err) {
-          return err.msg == 'did it broke?';
+    this.add(
+      'route:mom.sally',
+      Route.extend({
+        model() {
+          step(assert, 1, 'MomSallyRoute#model');
+          return RSVP.reject({
+            msg: 'did it broke?',
+          });
         },
-        'Correct error was thrown'
-      );
-    }
+        actions: {
+          error() {
+            step(assert, 2, 'MomSallyRoute#actions.error');
+            return true;
+          },
+        },
+      })
+    );
+
+    await assert.rejects(
+      this.visit('/grandma/mom/sally'),
+      function (err) {
+        return err.msg == 'did it broke?';
+      },
+      'Correct error was thrown'
+    );
+  }
 
-    async [`@test Handled errors that are thrown through rejection aren't swallowed`](assert) {
-      await this.visit('/');
+  async [`@test Handled errors that are thrown through rejection aren't swallowed`](assert) {
+    await this.visit('/');
 
-      let handledError;
+    let handledError;
 
-      this.add(
-        'route:mom.sally',
-        Route.extend({
-          model() {
-            step(assert, 1, 'MomSallyRoute#model');
-            return RSVP.reject({
-              msg: 'did it broke?',
-            });
-          },
-          actions: {
-            error(err) {
-              step(assert, 2, 'MomSallyRoute#actions.error');
-              handledError = err;
-              expectDeprecation(() => {
-                this.transitionTo('mom.this-route-throws');
-              }, /Calling transitionTo on a route is deprecated/);
-
-              return false;
-            },
-          },
-        })
-      );
+    this.add(
+      'route:mom.sally',
+      Route.extend({
+        model() {
+          step(assert, 1, 'MomSallyRoute#model');
+          return RSVP.reject({
+            msg: 'did it broke?',
+          });
+        },
+        actions: {
+          error(err) {
+            step(assert, 2, 'MomSallyRoute#actions.error');
+            handledError = err;
+            expectDeprecation(() => {
+              this.transitionTo('mom.this-route-throws');
+            }, /Calling transitionTo on a route is deprecated/);
 
-      this.add(
-        'route:mom.this-route-throws',
-        Route.extend({
-          model() {
-            step(assert, 3, 'MomThisRouteThrows#model');
-            return RSVP.reject(handledError);
+            return false;
           },
-        })
-      );
-
-      await assert.rejects(
-        this.visit('/grandma/mom/sally'),
-        function (err) {
-          return err.msg === 'did it broke?';
         },
-        'it broke'
-      );
-    }
+      })
+    );
+
+    this.add(
+      'route:mom.this-route-throws',
+      Route.extend({
+        model() {
+          step(assert, 3, 'MomThisRouteThrows#model');
+          return RSVP.reject(handledError);
+        },
+      })
+    );
+
+    await assert.rejects(
+      this.visit('/grandma/mom/sally'),
+      function (err) {
+        return err.msg === 'did it broke?';
+      },
+      'it broke'
+    );
+  }
 
-    async ['@test Default error events move into nested route, prioritizing more specifically named error routes - NEW'](
-      assert
-    ) {
-      await this.visit('/');
-
-      this.addTemplate('grandma.error', 'ERROR: {{@model.msg}}');
-      this.addTemplate('mom_error', 'MOM ERROR: {{@model.msg}}');
-
-      this.add(
-        'route:mom.sally',
-        Route.extend({
-          model() {
-            step(assert, 1, 'MomSallyRoute#model');
-            return RSVP.reject({
-              msg: 'did it broke?',
-            });
-          },
-          actions: {
-            error() {
-              step(assert, 2, 'MomSallyRoute#actions.error');
-              return true;
-            },
+  async ['@test Default error events move into nested route, prioritizing more specifically named error routes - NEW'](
+    assert
+  ) {
+    await this.visit('/');
+
+    this.addTemplate('grandma.error', 'ERROR: {{@model.msg}}');
+    this.addTemplate('mom_error', 'MOM ERROR: {{@model.msg}}');
+
+    this.add(
+      'route:mom.sally',
+      Route.extend({
+        model() {
+          step(assert, 1, 'MomSallyRoute#model');
+          return RSVP.reject({
+            msg: 'did it broke?',
+          });
+        },
+        actions: {
+          error() {
+            step(assert, 2, 'MomSallyRoute#actions.error');
+            return true;
           },
-        })
-      );
-
-      await this.visit('/grandma/mom/sally');
-
-      step(assert, 3, 'Application finished booting');
+        },
+      })
+    );
 
-      assert.equal(
-        this.$('#app').text(),
-        'GRANDMA MOM ERROR: did it broke?',
-        'the more specifically named mome error substate was entered over the other error route'
-      );
+    await this.visit('/grandma/mom/sally');
 
-      assert.equal(this.currentPath, 'grandma.mom_error', 'Initial route fully loaded');
-    }
+    step(assert, 3, 'Application finished booting');
 
-    async ['@test Slow promises waterfall on startup'](assert) {
-      await this.visit('/');
+    assert.equal(
+      this.$('#app').text(),
+      'GRANDMA MOM ERROR: did it broke?',
+      'the more specifically named mome error substate was entered over the other error route'
+    );
 
-      let grandmaDeferred = RSVP.defer();
-      let sallyDeferred = RSVP.defer();
+    assert.equal(this.currentPath, 'grandma.mom_error', 'Initial route fully loaded');
+  }
 
-      this.addTemplate('loading', 'LOADING');
-      this.addTemplate('mom', 'MOM {{outlet}}');
-      this.addTemplate('mom.loading', 'MOMLOADING');
-      this.addTemplate('mom.sally', 'SALLY');
+  async ['@test Slow promises waterfall on startup'](assert) {
+    await this.visit('/');
 
-      this.add(
-        'route:grandma',
-        Route.extend({
-          model() {
-            step(assert, 1, 'GrandmaRoute#model');
-            return grandmaDeferred.promise;
-          },
-        })
-      );
+    let grandmaDeferred = RSVP.defer();
+    let sallyDeferred = RSVP.defer();
 
-      this.add(
-        'route:mom',
-        Route.extend({
-          model() {
-            step(assert, 2, 'MomRoute#model');
-            return {};
-          },
-        })
-      );
+    this.addTemplate('loading', 'LOADING');
+    this.addTemplate('mom', 'MOM {{outlet}}');
+    this.addTemplate('mom.loading', 'MOMLOADING');
+    this.addTemplate('mom.sally', 'SALLY');
 
-      this.add(
-        'route:mom.sally',
-        Route.extend({
-          model() {
-            step(assert, 3, 'SallyRoute#model');
-            return sallyDeferred.promise;
-          },
-          setupController() {
-            step(assert, 4, 'SallyRoute#setupController');
-          },
-        })
-      );
+    this.add(
+      'route:grandma',
+      Route.extend({
+        model() {
+          step(assert, 1, 'GrandmaRoute#model');
+          return grandmaDeferred.promise;
+        },
+      })
+    );
+
+    this.add(
+      'route:mom',
+      Route.extend({
+        model() {
+          step(assert, 2, 'MomRoute#model');
+          return {};
+        },
+      })
+    );
+
+    this.add(
+      'route:mom.sally',
+      Route.extend({
+        model() {
+          step(assert, 3, 'SallyRoute#model');
+          return sallyDeferred.promise;
+        },
+        setupController() {
+          step(assert, 4, 'SallyRoute#setupController');
+        },
+      })
+    );
 
-      let promise = runTask(() => this.visit('/grandma/mom/sally')).then(() => {
-        text = this.$('#app').text();
+    let promise = runTask(() => this.visit('/grandma/mom/sally')).then(() => {
+      text = this.$('#app').text();
 
-        assert.equal(text, 'GRANDMA MOM SALLY', `Sally template displayed`);
-      });
-      let text = this.$('#app').text();
+      assert.equal(text, 'GRANDMA MOM SALLY', `Sally template displayed`);
+    });
+    let text = this.$('#app').text();
 
-      assert.equal(
-        text,
-        'LOADING',
-        `The loading template is nested in application template's outlet`
-      );
+    assert.equal(
+      text,
+      'LOADING',
+      `The loading template is nested in application template's outlet`
+    );
 
-      runTask(() => grandmaDeferred.resolve());
-      text = this.$('#app').text();
+    runTask(() => grandmaDeferred.resolve());
+    text = this.$('#app').text();
 
-      assert.equal(
-        text,
-        'GRANDMA MOM MOMLOADING',
-        `Mom's child loading route is displayed due to sally's slow promise`
-      );
+    assert.equal(
+      text,
+      'GRANDMA MOM MOMLOADING',
+      `Mom's child loading route is displayed due to sally's slow promise`
+    );
 
-      sallyDeferred.resolve();
+    sallyDeferred.resolve();
 
-      return promise;
-    }
+    return promise;
+  }
 
-    async ['@test Enter child loading state of pivot route'](assert) {
-      await this.visit('/');
+  async ['@test Enter child loading state of pivot route'](assert) {
+    await this.visit('/');
 
-      let deferred = RSVP.defer();
-      this.addTemplate('grandma.loading', 'GMONEYLOADING');
+    let deferred = RSVP.defer();
+    this.addTemplate('grandma.loading', 'GMONEYLOADING');
 
-      this.add(
-        'route:mom.sally',
-        Route.extend({
-          setupController() {
-            step(assert, 1, 'SallyRoute#setupController');
-          },
-        })
-      );
+    this.add(
+      'route:mom.sally',
+      Route.extend({
+        setupController() {
+          step(assert, 1, 'SallyRoute#setupController');
+        },
+      })
+    );
+
+    this.add(
+      'route:grandma.puppies',
+      Route.extend({
+        model() {
+          return deferred.promise;
+        },
+      })
+    );
 
-      this.add(
-        'route:grandma.puppies',
-        Route.extend({
-          model() {
-            return deferred.promise;
-          },
-        })
-      );
+    await this.visit('/grandma/mom/sally');
+    assert.equal(this.currentPath, 'grandma.mom.sally', 'Initial route fully loaded');
 
-      await this.visit('/grandma/mom/sally');
-      assert.equal(this.currentPath, 'grandma.mom.sally', 'Initial route fully loaded');
+    let promise = runTask(() => this.visit('/grandma/puppies')).then(() => {
+      assert.equal(this.currentPath, 'grandma.puppies', 'Finished transition');
+    });
 
-      let promise = runTask(() => this.visit('/grandma/puppies')).then(() => {
-        assert.equal(this.currentPath, 'grandma.puppies', 'Finished transition');
-      });
+    assert.equal(this.currentPath, 'grandma.loading', `in pivot route's child loading state`);
+    deferred.resolve();
 
-      assert.equal(this.currentPath, 'grandma.loading', `in pivot route's child loading state`);
-      deferred.resolve();
+    return promise;
+  }
 
-      return promise;
-    }
+  async [`@test Error events that aren't bubbled don't throw application assertions`](assert) {
+    await this.visit('/');
 
-    async [`@test Error events that aren't bubbled don't throw application assertions`](assert) {
-      await this.visit('/');
-
-      this.add(
-        'route:mom.sally',
-        Route.extend({
-          model() {
-            step(assert, 1, 'MomSallyRoute#model');
-            return RSVP.reject({
-              msg: 'did it broke?',
-            });
-          },
-          actions: {
-            error(err) {
-              step(assert, 2, 'MomSallyRoute#actions.error');
-              assert.equal(err.msg, 'did it broke?', `it didn't break`);
-              return false;
-            },
+    this.add(
+      'route:mom.sally',
+      Route.extend({
+        model() {
+          step(assert, 1, 'MomSallyRoute#model');
+          return RSVP.reject({
+            msg: 'did it broke?',
+          });
+        },
+        actions: {
+          error(err) {
+            step(assert, 2, 'MomSallyRoute#actions.error');
+            assert.equal(err.msg, 'did it broke?', `it didn't break`);
+            return false;
           },
-        })
-      );
+        },
+      })
+    );
 
-      return this.visit('/grandma/mom/sally');
-    }
+    return this.visit('/grandma/mom/sally');
+  }
 
-    ['@test Handled errors that bubble can be handled at a higher level'](assert) {
-      let handledError;
-
-      this.add(
-        'route:mom',
-        Route.extend({
-          actions: {
-            error(err) {
-              step(assert, 3, 'MomRoute#actions.error');
-              assert.equal(
-                err,
-                handledError,
-                `error handled and rebubbled is handleable at higher route`
-              );
-            },
+  ['@test Handled errors that bubble can be handled at a higher level'](assert) {
+    let handledError;
+
+    this.add(
+      'route:mom',
+      Route.extend({
+        actions: {
+          error(err) {
+            step(assert, 3, 'MomRoute#actions.error');
+            assert.equal(
+              err,
+              handledError,
+              `error handled and rebubbled is handleable at higher route`
+            );
           },
-        })
-      );
+        },
+      })
+    );
+
+    this.add(
+      'route:mom.sally',
+      Route.extend({
+        model() {
+          step(assert, 1, 'MomSallyRoute#model');
+          return RSVP.reject({
+            msg: 'did it broke?',
+          });
+        },
+        actions: {
+          error(err) {
+            step(assert, 2, 'MomSallyRoute#actions.error');
+            handledError = err;
 
-      this.add(
-        'route:mom.sally',
-        Route.extend({
-          model() {
-            step(assert, 1, 'MomSallyRoute#model');
-            return RSVP.reject({
-              msg: 'did it broke?',
-            });
+            return true;
           },
-          actions: {
-            error(err) {
-              step(assert, 2, 'MomSallyRoute#actions.error');
-              handledError = err;
+        },
+      })
+    );
 
-              return true;
-            },
-          },
-        })
-      );
+    return this.visit('/grandma/mom/sally');
+  }
 
-      return this.visit('/grandma/mom/sally');
-    }
+  async ['@test Setting a query param during a slow transition should work'](assert) {
+    await this.visit('/');
 
-    async ['@test Setting a query param during a slow transition should work'](assert) {
-      await this.visit('/');
+    let deferred = RSVP.defer();
+    this.addTemplate('memere.loading', 'MMONEYLOADING');
 
-      let deferred = RSVP.defer();
-      this.addTemplate('memere.loading', 'MMONEYLOADING');
+    this.add(
+      'route:grandma',
+      Route.extend({
+        beforeModel: function () {
+          expectDeprecation(() => {
+            this.transitionTo('memere', 1);
+          }, /Calling transitionTo on a route is deprecated/);
+        },
+      })
+    );
+
+    this.add(
+      'route:memere',
+      Route.extend({
+        queryParams: {
+          test: { defaultValue: 1 },
+        },
+      })
+    );
+
+    this.add(
+      'route:memere.index',
+      Route.extend({
+        model() {
+          return deferred.promise;
+        },
+      })
+    );
 
-      this.add(
-        'route:grandma',
-        Route.extend({
-          beforeModel: function () {
-            expectDeprecation(() => {
-              this.transitionTo('memere', 1);
-            }, /Calling transitionTo on a route is deprecated/);
-          },
-        })
-      );
+    let promise = runTask(() => this.visit('/grandma')).then(() => {
+      assert.equal(this.currentPath, 'memere.index', 'Transition should be complete');
+    });
+    let memereController = this.getController('memere');
 
-      this.add(
-        'route:memere',
-        Route.extend({
-          queryParams: {
-            test: { defaultValue: 1 },
-          },
-        })
-      );
+    assert.equal(this.currentPath, 'memere.loading', 'Initial route should be loading');
 
-      this.add(
-        'route:memere.index',
-        Route.extend({
-          model() {
-            return deferred.promise;
-          },
-        })
-      );
+    memereController.set('test', 3);
 
-      let promise = runTask(() => this.visit('/grandma')).then(() => {
-        assert.equal(this.currentPath, 'memere.index', 'Transition should be complete');
-      });
-      let memereController = this.getController('memere');
+    assert.equal(this.currentPath, 'memere.loading', 'Initial route should still be loading');
 
-      assert.equal(this.currentPath, 'memere.loading', 'Initial route should be loading');
+    assert.equal(
+      memereController.get('test'),
+      3,
+      'Controller query param value should have changed'
+    );
+    deferred.resolve();
 
-      memereController.set('test', 3);
+    return promise;
+  }
+}
 
-      assert.equal(this.currentPath, 'memere.loading', 'Initial route should still be loading');
+moduleFor('Loading/Error Substates', LoadingErrorSubstatesTests);
 
-      assert.equal(
-        memereController.get('test'),
-        3,
-        'Controller query param value should have changed'
-      );
-      deferred.resolve();
+moduleFor('Loading/Error Substates - nested routes', LoadingErrorSubstatesNestedTests);
 
-      return promise;
+moduleFor(
+  'Loading/Error Substates (simulated within lazy engine)',
+  class extends LoadingErrorSubstatesTests {
+    get routerOptions() {
+      return lazyLoadingRouterOptions;
+    }
+  }
+);
+
+moduleFor(
+  'Loading/Error Substates - nested routes (simulated within lazy engine)',
+  class extends LoadingErrorSubstatesNestedTests {
+    get routerOptions() {
+      return lazyLoadingRouterOptions;
     }
   }
 );
diff --git a/packages/internal-test-helpers/index.js b/packages/internal-test-helpers/index.js
index bad3852df84..9000da6f269 100644
--- a/packages/internal-test-helpers/index.js
+++ b/packages/internal-test-helpers/index.js
@@ -7,6 +7,7 @@ export { default as moduleFor, setupTestClass } from './lib/module-for';
 export { default as strip } from './lib/strip';
 export { default as applyMixins } from './lib/apply-mixins';
 export { default as getTextOf } from './lib/get-text-of';
+export { default as lazyLoadingRouterOptions } from './lib/lazy-router-options';
 export {
   expectDeprecation,
   expectNoDeprecation,
diff --git a/packages/internal-test-helpers/lib/lazy-router-options.ts b/packages/internal-test-helpers/lib/lazy-router-options.ts
new file mode 100644
index 00000000000..ee5bd0f12af
--- /dev/null
+++ b/packages/internal-test-helpers/lib/lazy-router-options.ts
@@ -0,0 +1,42 @@
+import { Router } from '@ember/-internals/routing';
+import { isDestroying } from '@glimmer/destroyable';
+import RSVP from 'rsvp';
+
+export default class LazyRouter extends Router {
+  location = 'none';
+  setupRouter(): boolean {
+    const result = super.setupRouter();
+    let getRoute = this._routerMicrolib.getRoute;
+    this._enginePromises = Object.create(null);
+    this._resolvedEngines = Object.create(null);
+
+    let routes = new Map();
+    let routePromises = new Map();
+    this._routerMicrolib.getRoute = (name) => {
+      if (routes.has(name)) {
+        return routes.get(name);
+      }
+
+      if (routePromises.has(name)) {
+        return routePromises.get(name);
+      }
+
+      let promise = new RSVP.Promise((resolve) => {
+        setTimeout(() => {
+          if (isDestroying(this)) {
+            return;
+          }
+
+          let route = getRoute(name);
+
+          routes.set(name, route);
+          resolve(route);
+        }, 10);
+      });
+      routePromises.set(name, promise);
+
+      return promise;
+    };
+    return result;
+  }
+}
diff --git a/packages/internal-test-helpers/lib/test-cases/application.js b/packages/internal-test-helpers/lib/test-cases/application.js
index 53ce885d1ce..a19182e26d3 100644
--- a/packages/internal-test-helpers/lib/test-cases/application.js
+++ b/packages/internal-test-helpers/lib/test-cases/application.js
@@ -15,7 +15,10 @@ export default class ApplicationTestCase extends TestResolverApplicationTestCase
     this.resolver = this.application.__registry__.resolver;
 
     if (this.resolver) {
-      this.resolver.add('router:main', Router.extend(this.routerOptions));
+      let { routerOptions } = this;
+      let RouterSubclass =
+        typeof routerOptions === 'function' ? routerOptions : Router.extend(routerOptions);
+      this.resolver.add('router:main', RouterSubclass);
     }
   }
 
diff --git a/packages/internal-test-helpers/lib/test-cases/autoboot-application.js b/packages/internal-test-helpers/lib/test-cases/autoboot-application.js
index a1b09057290..c2cbbf1cbab 100644
--- a/packages/internal-test-helpers/lib/test-cases/autoboot-application.js
+++ b/packages/internal-test-helpers/lib/test-cases/autoboot-application.js
@@ -10,7 +10,10 @@ export default class AutobootApplicationTestCase extends TestResolverApplication
     this.resolver = application.__registry__.resolver;
 
     if (this.resolver) {
-      this.resolver.add('router:main', Router.extend(this.routerOptions));
+      let { routerOptions } = this;
+      let RouterSubclass =
+        typeof routerOptions === 'function' ? routerOptions : Router.extend(routerOptions);
+      this.resolver.add('router:main', RouterSubclass);
     }
 
     return application;