codelord.net

Code, Angular, iOS and more by Aviv Ben-Yosef

Circular Dependencies in Angular and the Injector Service

| Comments

Angular’s dependency injection is a great boon to productivity, but even it has its limits.

While not an everyday occurrence, it is quite possible to come across a circular dependency. This happens when service A injects service B, but service B in turn injects service A, usually indirectly. For example, B depends on service C which depends on A – A -> B -> C -> A forms a nice little circle.

When Angular’s bootstrap process tries to setup all the services and inject each service’s dependencies, it detects when such a scenario happens and errors out, instead of getting stuck in an infinite loop.

In most cases, circular dependencies are code smells for design that could be made clearer. Most likely you have a service that’s gotten too big, and splitting it will result in cleaner code and no circular dependency.

But, there are a few common scenarios that come up in a lot of apps where some kind of circular dependency makes sense. Let’s look at an example and a solution.

A real-world circular dependency

Enter HTTP interceptors. Interceptors are Angular’s very handy tool for handling cross-app concerns when it comes to handling HTTP requests and responses. They are probably most often used for handling authentication.

I’ve come across circular dependencies showing up in interceptors at several clients. It usually goes something like this:

As part of implementing the authentication mechanism of the app, we create an interceptor to be in charge of handling the different responses. One of the behaviors it needs depends on an external service, which in turn makes an HTTP request.

For example, we would like to redirect the user to a login page on every 401 error. An interceptor watches for these errors, and once it sees them it calls our AuthService to tell it to handle an expired session. The same AuthService depends on $http for some reason, like performing a login request.

A naive setup would look somewhat like this:

1
2
3
4
5
6
7
8
9
10
appModule.factory('AuthService', function($http) {
  return {
    login: function(user, password) {
      // This uses $http to login
    },
    handleExpiredSession: function() {
      // Redirect to login page
    }
  };
});
1
2
3
4
5
6
7
8
9
appModule.config(function($httpProvider) {
  $httpProvider.interceptors.push(function(AuthService) {
    return {
      response: function(response) {
        // Detect and handle 401 errors
      }
    };
  });
});

If you’ll try running an app with this code, Angular will spit out an error: Error: [$injector:cdep] Circular dependency found: $http <- AuthService <- $http.

That’s because $http depends on our interceptor, which depends on AuthService, which depends on $http. (Are you getting dizzy, too?)

$injector to the rescue

Just for cases like these Angular provides us with the $injector service. The injector is the programmatic way to access Angular’s dependency injection.

Using it, we can manually inject AuthService inside our interceptor and break the circular dependency:

1
2
3
4
5
6
7
8
9
10
appModule.config(function($httpProvider) {
  $httpProvider.interceptors.push(function($injector) {
    return {
      response: function(response) {
        // Detect and handle 401 errors
        $injector.get('AuthService').handleExpiredSession();
      }
    };
  });
});

Calling $injector.get('AuthService') will return the exact same singleton instance of AuthService. The main difference is that now we are performing this at a later point, after Angular has finished bootstrapping the project. At this point in time, where everything is up and running, it’s safe to inject AuthService.

Thus we have effectively broken out of the circular dependency.

Voila!

“Maintaining AngularJS feels like Cobol 🤷…”

You want to do AngularJS the right way.
Yet every blog post you see makes it look like your codebase is obsolete. Components? Lifecycle hooks? Controllers are dead?

It would be great to work on a modern codebase again, but who has weeks for a rewrite?
Well, you can get your app back in shape, without pushing back all your deadlines! Imagine, upgrading smoothly along your regular tasks, no longer deep in legacy.

Subscribe and get my free email course with steps for upgrading your AngularJS app to the latest 1.6 safely and without a rewrite.

Get the modernization email course!

Comments