Troubleshooting Angular routing

Maria Korneeva
ngconf
Published in
7 min readJun 27, 2022
criminal girl with Angular logo

While the documentation of Angular routing is great, it does not cover some edge cases and troubleshooting of missing imports or duplicate paths in different submodules. So, in this story I decided to describe the ways you can break it. We will learn what can go wrong when configuring Angular routing and — through pain and suffering — how Angular routing works.

First of all, let’s get a working example. To do so, you can follow the official Angular tutorial on routing or check out the following code. Here is a quick step-by-step instruction. Firstly, we should create a routing-module:

//app-routing.module.tsconst routes: Routes = [
{ path: ‘one’, component: OneComponent },
{ path: ‘two’, component: TwoComponent },
{ path: ‘’, redirectTo: ‘/one’, pathMatch: ‘full’ },
{ path: ‘**’, component: RouteNotFoundComponent },
];
@NgModule({
imports: [BrowserModule, RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class RoutingModule {}

Let’s have a look at our routes. Their order is important because the Router uses a first-match wins strategy when matching routes, so more specific routes should be placed above less specific routes. List routes with a static path first, followed by an empty path route, which matches the default route. The wildcard route comes last because it matches every URL and the Router selects it only if no other routes match first.

Don’t forget to export RouterModule in app-routing.module.ts and import RoutingModule into app.module:

//app.module.ts@NgModule({
imports: [BrowserModule, RoutingModule],
declarations: [
AppComponent,
OneComponent,
TwoComponent,
RouteNotFoundComponent,
],
bootstrap: [AppComponent],
})
export class AppModule {}

Side note: you can put your routes configuration into app.module.ts if it is small and clear. Putting routing configurations into a separate module has no technical meaning and is done for clearness and readability purposes only.

The only missing part is <router-outlet> which should be placed in the bootstrap component (AppComponent) or index.html.

//app.component.html<p>Start editing to see some magic happen :)</p>
<router-outlet></router-outlet>

Now that we have our routing up and running, let’s break it and see what happens.

Absolute path in the routes?

As you might have noticed, we used the routes without slashes in our routes configuration. What if we add some? Our routes would look the following way:

const routes: Routes = [
{ path: ‘one’, component: OneComponent },
{ path: ‘/two’, component: TwoComponent },
{ path: ‘’, redirectTo: ‘/one’, pathMatch: ‘full’ },
{ path: ‘**’, component: RouteNotFoundComponent },
];

When we bootstrap our app, an error is seen: ‘Error: Invalid configuration of route ‘/two’: path cannot start with a slash’. Okaaaay, it didn’t go unnoticed. We have to find a different way how to break Angular routing.

Different components, one route?

Let’s start simple and check out what happens if we provide 2 different routes for the same component:

const routes: Routes = [
{ path: ‘one’, component: OneComponent },
{ path: ‘two’, component: TwoComponent },
{ path: ‘three’, component: OneComponent },
{ path: ‘**’, component: RouteNotFoundComponent },
];

Unspectacularly, nothing special happens. When we call https://localhost:4200/one we see OneComponent. The same happens when we call https://localhost:4200/three. Here is the code example for this evil use case.

Let’s get more evil and provide 2 components for the same route. Let’s see how Angular deals with this.

Different routes, one component?

For this test we will configure our routes as follows:

const routes: Routes = [
{ path: ‘one’, component: OneComponent },
{ path: ‘two’, component: TwoComponent },
{ path: ‘one’, component: RouteNotFoundComponent },
{ path: ‘**’, component: RouteNotFoundComponent },
];

This should confuse Angular, as it should not be clear what component to load for the given route. However, Angular can master this, too. If we request https://localhost:4200/one, we see OneComponent, no matter how often we re-load the page. Here is the code example.

A careful reader has already guessed that Angular applies a first-match wins strategy, so that the second case is completely ignored and does not confuse the framework. So we have to come up with some more serious threats.

What if endless redirect happens?

As you’ve probably noticed, I’ve got the router configuration which is responsible for the redirect to the default page ( { path: ‘’, redirectTo: ‘/one’, pathMatch: ‘full’ }). What if the default page redirects back so that an endless redirect happens? Let’s try it out!

To test it, let’s configure our routes as follows:

const routes: Routes = [
{ path: ‘one’, component: OneComponent },
{ path: ‘two’, component: TwoComponent },
{ path: ‘three’, redirectTo: ‘/four’, pathMatch: ‘full’ },
{ path: ‘four’, redirectTo: ‘/three’, pathMatch: ‘full’ },

{ path: ‘**’, component: RouteNotFoundComponent },
];

*evil laugh*

So we expect our Angular application to be caught in the eternal routing redirect hell. However, when we call https://localhost:4200/three we see the internals of our RouteNotFoundComponent instead of buffer overflow. Here is the code example.

Why? It is as simple as that: “Note that no further redirects are evaluated after an absolute redirect.” So if Angular does not find any component after the first redirect we are out of the loop. However, it does not mean that we cannot get infinite loops. For examples, here is a Stackoverflow issue.

Till now it was quite easy. Let’s add some child routes and see what happens. Usually, your configuration for nested routes would look like this:

const routes: Routes = [{
path: ‘one’,
component: OneComponent,
children: [
{ path: ‘one-a’, component: OneAComponent },
{ path: ‘one-b’, component: OneBComponent },

],
},
...
];

Child component is the parent of itself?

So let’s go on with our destructive plan and nest our parent component as a child:

const routes: Routes = [{
path: ‘’,
component: OneComponent,
children: [{ path: ‘one’, component: OneComponent }],
},
...
];

Again, Angular behaves quite predictably, nesting OneComponent into OneComponent, displaying it twice if you call https://localhost:4200/one. Here is the link to the code. It changes, however, if you add pathMatch: ‘full’ to the parent route as it no longer matches children’s path.

Same route, different logic?

Though you probably already know how Angular will behave in the following situation, let’s still briefly check on it:

const routes: Routes = [{
path: ‘’,
component: OneComponent,
children: [{ path: ‘one’, component: OneAComponent }],
},
{
path: ‘one’,
component: OneComponent,
children: [{ path: ‘’, component: OneBComponent }],
},
...
];

So basically, https://localhost:4200/one can be our parent route and our child route. This would result in 2 different components being mapped to this route. However, due to the first-match wins strategy it is consistently interpreted as OneAComponent. Here is the code example.

No problem, we will find the way how to break it. Let’s have a look at lazy loading. Maybe we will be more successful there.

For lazy-loaded modules we need to re-configure our app.module.ts as follows:

const routes: Routes = [
{
path: ‘one’,
loadChildren: () => import(‘./one/one.module’)
.then((m) => m.OneModule),

},
{ path: ‘two’, component: TwoComponent },
{ path: ‘’, redirectTo: ‘/one’, pathMatch: ‘full’ },
{ path: ‘**’, component: RouteNotFoundComponent },
];

And here is our new lazy-loaded module:

const routes: Routes = [
{
path: ‘’,
component: OneComponent,
children: [
{ path: ‘one-a’, component: OneAComponent },
{ path: ‘one-b’, component: OneBComponent },
],
},
{ path: ‘’, redirectTo: ‘/one’, pathMatch: ‘full’ },
{ path: ‘**’, component: OneComponent },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
declarations: [OneComponent, OneAComponent, OneBComponent],
})
export class OneModule {}

If you have questions regarding the working example, please consult the official documentation.

Same route — eagerly- and lazy-loaded?

Let’s reconfigure our routes to use the same path in the eagerly- and lazy-loaded modules:

const routes: Routes = [
{
path: ‘one’,
loadChildren: () => import(‘./one/one.module’)
.then((m) => m.OneModule),
},
{ path: ‘one’, children: [{ path: ‘one-a’, component: TwoComponent }] },
...
];

No surprise here — Angular consistently follows first come first serve principle. The component that matches first gets displayed. Change the order of the route if you want to test it. The same is actually valid for two submodules with the same routes, too. It is all about the order.

forRoot() instead of forChild()?

What if we use forRoot() in our lazy-loaded module? Well, Angular will respond with “Error: Uncaught (in promise): Error: RouterModule.forRoot() called twice. Lazy loaded modules should use RouterModule.forChild() instead.
Error: RouterModule.forRoot() called twice. Lazy loaded modules should use RouterModule.forChild() instead.

We tried really hard to implement things in the wrong way. However, Angular catches and handles these errors or misconceptions gracefully. Here is the summary of possible troubles:

  • ‘router-outlet’ is not a known element — did you export RouterModule in your app-routing.module.ts?
  • No matched component is displayed — did you add <router-outlet> to your app.component.html, index.html or the entry component of the lazy-loaded module? If it still doesn’t work — have you declared your components in the corresponding module?
  • No matched lazy-loaded nested components are displayed — make sure that either your lazy-loaded component has empty string (‘’) as the path or your app-routing. Otherwise your component might be mapped to https://localhost:4200/one/one/one-a.
  • Wrong component is being mapped — make sure you use pathMatch: ‘full’ and { path: ‘**’, component: RouteNotFoundComponent } correctly. The order matters!
  • As usual — check for typos in your paths!
  • And if some deep analysis is required, enable verbose mode for your Router with the following line of code:
imports: [
RouterModule.forRoot(
routes,
{ enableTracing: true } // <-- debugging purposes only
)
]

Even more options and configurations are described in the official documentation.

Happy routing!

--

--

Maria Korneeva
Maria Korneeva

Written by Maria Korneeva

Learning tech hacks by sharing them with you— that is what drives me. #learningbysharing