AngularJS
third-party library
asynchronous loading
service wrapper
JavaScript development

AngularJS - wrapping 3rd party asynchronic loaded library as a service

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In AngularJS, third-party libraries work best when the rest of the application does not touch global state directly. That becomes more important when the library is loaded asynchronously, because consumers must wait until the script is available. The usual fix is to hide the loading and readiness logic inside a service that returns a promise.

Why a Service Wrapper Helps

If controllers or directives call window.SomeLibrary directly, two problems appear immediately. First, load order becomes fragile: the controller may run before the script has finished loading. Second, testing becomes harder because every test now depends on a real browser global.

A service wrapper solves both problems:

  • it loads the script once
  • it exposes a promise-based API
  • it gives AngularJS code a stable dependency to inject
  • it makes mocking simple in unit tests

The goal is not just to "make it work". The goal is to make script loading predictable and keep the rest of the application unaware of how that script arrived.

Load the Script Once

The first service should be responsible only for loading the external file. A small loader built around $q, $document, and $window is enough.

javascript
1angular.module('app').factory('scriptLoader', function ($q, $document, $window) {
2  var cache = {};
3
4  return function loadScript(url, globalName) {
5    if (cache[url]) {
6      return cache[url];
7    }
8
9    var deferred = $q.defer();
10    cache[url] = deferred.promise;
11
12    if ($window[globalName]) {
13      deferred.resolve($window[globalName]);
14      return deferred.promise;
15    }
16
17    var script = $document[0].createElement('script');
18    script.src = url;
19    script.async = true;
20
21    script.onload = function () {
22      if ($window[globalName]) {
23        deferred.resolve($window[globalName]);
24      } else {
25        deferred.reject(new Error('Library loaded but global was not found'));
26      }
27    };
28
29    script.onerror = function () {
30      deferred.reject(new Error('Failed to load ' + url));
31    };
32
33    $document[0].body.appendChild(script);
34    return deferred.promise;
35  };
36});

This pattern matters for two reasons. It caches the promise so repeated injections do not insert the same script several times, and it rejects cleanly if the file fails to load.

Wrap the Library API, Not the Global

The second service should express the parts of the library your application actually uses. Suppose the external file creates a global called AwesomeLibrary with an init method and a search method. The AngularJS wrapper can expose those operations without leaking the global object to the rest of the app.

javascript
1angular.module('app').factory('awesomeService', function ($q, scriptLoader) {
2  var libraryPromise = scriptLoader('/vendor/awesome-library.js', 'AwesomeLibrary')
3    .then(function (AwesomeLibrary) {
4      return AwesomeLibrary.init({ apiKey: 'demo-key' }).then(function () {
5        return AwesomeLibrary;
6      });
7    });
8
9  return {
10    ready: function () {
11      return libraryPromise;
12    },
13    search: function (query) {
14      return libraryPromise.then(function (AwesomeLibrary) {
15        return AwesomeLibrary.search(query);
16      });
17    }
18  };
19});

Consumers now have a stable contract. They call awesomeService.search('term') and do not care whether the script was already loaded, still loading, or just initialized.

Consume It from AngularJS Code

The controller can remain small and predictable because the async work is centralized in the service.

javascript
1angular.module('app').controller('SearchController', function ($scope, awesomeService) {
2  $scope.query = '';
3  $scope.results = [];
4  $scope.error = null;
5
6  $scope.runSearch = function () {
7    $scope.error = null;
8
9    awesomeService.search($scope.query)
10      .then(function (results) {
11        $scope.results = results;
12      })
13      .catch(function (err) {
14        $scope.error = err.message;
15      });
16  };
17});

This is the right separation of concerns. The controller manages view state. The wrapper manages loading, initialization, and communication with the third-party code.

Keep AngularJS in the Loop

One subtle issue with third-party libraries is that callbacks may run outside AngularJS. If the library accepts callbacks instead of returning promises, wrap those callbacks in $q or trigger a digest safely with $rootScope.$evalAsync. Otherwise the data may change without the view updating.

A callback-based wrapper looks like this:

javascript
1angular.module('app').factory('awesomeCallbacks', function ($q, $rootScope, scriptLoader) {
2  return {
3    search: function (query) {
4      return scriptLoader('/vendor/awesome-library.js', 'AwesomeLibrary')
5        .then(function (AwesomeLibrary) {
6          var deferred = $q.defer();
7
8          AwesomeLibrary.search(query, function (err, results) {
9            $rootScope.$evalAsync(function () {
10              if (err) {
11                deferred.reject(err);
12              } else {
13                deferred.resolve(results);
14              }
15            });
16          });
17
18          return deferred.promise;
19        });
20    }
21  };
22});

That extra step is often the difference between an apparently correct wrapper and one that randomly leaves the screen stale.

Common Pitfalls

The most common mistake is resolving the promise as soon as the script file loads, even though the library still needs initialization. If the library has a separate startup step, the promise returned to AngularJS code should resolve only after initialization succeeds.

Another mistake is exposing the raw global object everywhere. That defeats the purpose of the service and spreads third-party details through the codebase.

Duplicate script insertion is another frequent bug. If two controllers request the library at the same time, a missing cache can append two identical script tags and create inconsistent state.

Finally, do not ignore digest integration. When callbacks happen outside AngularJS, the UI may not update until some unrelated event triggers a digest cycle.

Summary

  • Wrap async third-party libraries behind an AngularJS service
  • Use one service to load the script and cache the promise
  • Use another service to expose the library operations your app needs
  • Resolve readiness only after both loading and initialization complete
  • Keep controllers unaware of browser globals
  • Use $q and $evalAsync when callbacks run outside AngularJS

Course illustration
Course illustration

All Rights Reserved.