Question

How AngularJS Data Binding and the Digest Cycle Work

javascriptangularjsdata-binding

Question

How does data binding work in AngularJS?

It is reasonably clear how AngularJS propagates changes from a view to a model. However, how does AngularJS detect changes to model properties without JavaScript property getters and setters?

For example, how does AngularJS know that this assignment occurred and update a bound view?

myobject.myproperty = "new value";

I have read about JavaScript watchers, but some older browser features are not supported in Internet Explorer 6 and Internet Explorer 7. How does AngularJS detect model changes in those environments?

Short Answer

AngularJS 1.x uses dirty checking, not property setters, getters, or browser-level property-change events. It repeatedly evaluates registered watch expressions during a digest cycle and compares each current value with its previously recorded value. When a value differs, AngularJS updates bindings and runs relevant listeners. Changes made through AngularJS-managed events automatically start a digest; changes from external code usually require $scope.$apply() or an AngularJS wrapper such as $timeout.

Concept

AngularJS data binding is built around three closely related ideas:

  • A model: values stored on a scope, such as user.name.
  • A watcher: a function that reads a value and remembers its previous result.
  • A digest cycle: the process in which AngularJS runs watchers and looks for changed values.

For a template binding such as:

<p>{{ user.name }}</p>

AngularJS creates a watcher that can evaluate user.name. During a digest, it evaluates that expression, compares the result with the previously stored result, and updates the DOM if the value changed.

AngularJS 1.x does not need setters or getters for ordinary data binding. It does not intercept this assignment:

$scope.user.name = "Ada";

Instead, at the next digest, AngularJS evaluates $scope.user.name again and sees that it no longer matches its old value. This comparison-based approach is called dirty checking.

A digest normally begins after work AngularJS knows about, including:

  • AngularJS directives such as ng-click and ng-model
  • AngularJS services such as $http, $timeout, and $interval
  • Calls made through $scope.$apply()

This design worked in older browsers, including IE 6 and IE 7, because it relies on normal JavaScript expression evaluation rather than unsupported property observation APIs.

AngularJS is an end-of-life framework, but understanding its digest cycle is important when maintaining existing AngularJS applications.

Mental Model

Imagine AngularJS as an inspector making regular rounds through a checklist.

Each watcher is one item on the checklist:

“What is the current value of user.name?”

On the previous round, the inspector recorded "Grace". On the next round, it sees "Ada". Because the value is different, AngularJS knows something changed and refreshes the parts of the page that display it.

The assignment itself does not notify AngularJS:

$scope.user.name = "Ada";

AngularJS notices only when it performs its next inspection round: the digest cycle.

$apply() is like telling the inspector, “External code has finished changing things; please make a round now.”

Take Quiz

Syntax and Examples

Use scope properties in templates, and let AngularJS start digests from its directives and services.

<div ng-app="app" ng-controller="ProfileController">
  <p>Hello, {{ user.name }}!</p>
  <button ng-click="rename()">Rename</button>
</div>
angular.module("app", [])
  .controller("ProfileController", function ($scope) {
    $scope.user = {
      name: "Grace"
    };

    $scope.rename = function () {
      $scope.user.name = "Ada";
    };
  });

When the button is clicked:

  1. AngularJS runs rename() because ng-click belongs to AngularJS.
  2. The function changes $scope.user.name.
  3. AngularJS starts a digest after the click handler finishes.
  4. The watcher for {{ user.name }} sees the new value.
  5. The paragraph changes to Hello, Ada!.

To react to a value in JavaScript, register a watcher explicitly:

$scope.$watch("user.name", function (newValue, oldValue) {
  if (newValue !== oldValue) {
    console.log("Name changed from", oldValue, "to", newValue);
  }
});

A watch expression may be a string expression or a function:

$scope.$watch(
  function () {
    return $scope.user.name;
  },
  function (newValue, oldValue) {
    console.log(newValue, oldValue);
  }
);

Use $apply() when an external library or raw browser callback modifies scope data:

externalLibrary.on("profileUpdated", function (name) {
  $scope.$apply(function () {
    $scope.user.name = name;
  });
});

Do not call $apply() from code already invoked by AngularJS, such as an ng-click handler; AngularJS already schedules the digest there.

Step by Step Execution

Consider this controller and template:

<p>{{ account.status }}</p>
<button ng-click="activate()">Activate</button>
$scope.account = { status: "Pending" };

$scope.activate = function () {
  $scope.account.status = "Active";
};

Here is what happens step by step:

  1. AngularJS links the template to the scope.
  2. It registers a watcher for the expression account.status.
  3. During the first digest, the watcher evaluates to "Pending".
  4. AngularJS stores "Pending" as the last known value and renders it in the paragraph.
  5. The user clicks the button.
  6. ng-click calls activate().
  7. activate() assigns "Active" to $scope.account.status.
  8. Because ng-click is AngularJS-managed, AngularJS starts a digest.
  9. The watcher evaluates account.status again and now receives "Active".
  10. The previous value and current value differ, so AngularJS updates the paragraph.
  11. AngularJS repeats watcher checks until no watched value changes during a pass. This stabilization prevents dependent bindings from being left out of date.

If a watcher keeps changing a watched value every time it runs, AngularJS cannot stabilize and eventually throws a digest iteration error. Watch listeners should not continually mutate the values they watch.

Real World Use Cases

AngularJS dirty checking and digest scheduling appear in common application tasks:

  • Forms: ng-model copies user input into scope data, then bound validation messages and previews refresh.
  • HTTP requests: $http.get() receives server data, updates a controller model, and AngularJS refreshes the view.
  • Timers: $timeout() changes a countdown or notification state and automatically triggers a digest.
  • Third-party widgets: A map, payment widget, WebSocket client, or legacy jQuery plugin may invoke callbacks outside AngularJS. Wrap model updates in $apply() or use $timeout().
  • Live dashboards: New metrics update scope values; bindings redraw text, classes, and repeated rows.
  • Validation rules: A watcher can derive one field from another, although computed values are often clearer when calculated directly in controller methods or services.

Example with a WebSocket-style external callback:

socket.onmessage = function (event) {
  var message = JSON.parse(event.data);

  $scope.$apply(function () {
    $scope.latestMessage = message.text;
  });
};

Without $apply(), the value may change in memory but the AngularJS view may not refresh until some later AngularJS event happens.

Take Quiz

Real Codebase Usage

In production AngularJS code, developers generally avoid manually triggering digests unless integrating external code.

Prefer AngularJS-aware services

Use $timeout rather than the browser's setTimeout when the callback changes scope data:

$timeout(function () {
  $scope.message = "Saved";
}, 500);

$timeout schedules AngularJS-aware work, so the view updates automatically.

Integrate external callbacks carefully

For callbacks from non-AngularJS libraries, call $apply() around the model update:

mapWidget.onMarkerSelected(function (marker) {
  $scope.$apply(function () {
    $scope.selectedMarker = marker;
  });
});

If a digest may already be running, $evalAsync() is often safer for scheduling scope work:

mapWidget.onMarkerSelected(function (marker) {
  $scope.$evalAsync(function () {
    $scope.selectedMarker = marker;
  });
});

Use guard clauses in watchers

A watch listener may run during initialization, so avoid unnecessary work:

$scope.$watch("searchTerm", function (newValue, oldValue) {
  if (newValue === oldValue) {
    return;
  }

  searchService.recordSearch(newValue);
});

Keep watches inexpensive

Every watcher is evaluated during digest cycles. Real applications should avoid expensive computations directly in templates or watch functions. Compute reusable values once when appropriate, use one-time bindings for static values, and remove watchers that are no longer needed.

var stopWatching = $scope.$watch("session.user", function (user) {
  if (user) {
    initializeUser(user);
    stopWatching();
  }
});

Common Mistakes

Expecting a plain assignment to update the view immediately

This external callback changes the model, but AngularJS may not know to run a digest:

setTimeout(function () {
  $scope.status = "Complete";
}, 1000);

Use $timeout instead:

$timeout(function () {
  $scope.status = "Complete";
}, 1000);

Or use $apply() when you must use an external callback:

setTimeout(function () {
  $scope.$apply(function () {
    $scope.status = "Complete";
  });
}, 1000);

Calling $apply() inside ng-click

This is incorrect because AngularJS is already processing an AngularJS event:

$scope.save = function () {
  $scope.$apply(function () {
    $scope.saved = true;
  });
};

It can cause an [$rootScope:inprog] error. Write the assignment directly:

$scope.save = function () {
  $scope.saved = true;
};

Creating watchers that modify their own watched value

This can prevent the digest from stabilizing:

$scope.$watch("count", function () {
  $scope.count++;
});

Watch listeners should not repeatedly change the same watched data. Make updates in response to user actions or ensure the listener has a terminating condition.

Assuming $watch deeply tracks all object changes by default

A normal watch compares object references. Mutating a nested property may not be detected by a reference watch:

$scope.$watch("settings", function () {
  console.log("Settings reference changed");
});

$scope.settings.theme = "dark";

For a small object where deep watching is truly needed:

$scope.$watch("settings", function () {
  console.log("A nested setting changed");
}, true);

Deep watches can be expensive. Prefer watching a specific property such as settings.theme when possible.

Doing expensive work in templates

Avoid calling costly functions in bindings because AngularJS may evaluate them many times:

<!-- Avoid if calculateTotal is expensive -->
<p>{{ calculateTotal() }}</p>

Calculate the value at an appropriate point and bind the resulting property instead.

Comparisons

ApproachHow changes are detectedTypical AngularJS use
Dirty checkingAngularJS reevaluates watched expressions and compares old and new valuesStandard AngularJS 1.x binding
Getters and settersA property assignment runs custom code immediatelyNot required for AngularJS scopes
Browser property observationThe browser notifies code when a property changesNot the basis of AngularJS 1.x binding; older browser support was limited
$watchRegisters a scope expression to compare during digestsReacting to a particular model value in JavaScript
$watchCollectionShallowly observes array items or object propertiesLists and small collections where items may be added or removed
Deep $watch(..., true)Compares nested object dataSmall objects only; use cautiously because it costs more
$apply()Runs external work, then starts a digestIntegrating non-AngularJS callbacks
$digest()Checks the current scope and its descendantsLower-level API; usually avoid in application code

$apply() is usually preferred over directly calling $digest() because $apply() begins from the root scope and includes AngularJS error handling. In everyday application code, AngularJS services and directives should start most digests for you.

Take Quiz

Cheat Sheet

  • AngularJS 1.x binding uses dirty checking.
  • A watcher evaluates an expression and compares its current value with its previous value.
  • A digest cycle runs watchers until values stabilize.
  • AngularJS does not need property setters/getters to notice normal model changes.
  • ng-click, ng-model, $http, $timeout, and $interval trigger AngularJS-aware updates.
  • External callbacks do not necessarily trigger a digest; use $apply() or $evalAsync().
// Register a watcher
$scope.$watch("user.name", function (newValue, oldValue) {
  // Respond to a changed value
});

// External callback integration
externalApi.onChange(function (value) {
  $scope.$apply(function () {
    $scope.value = value;
  });
});

// AngularJS-aware delay
$timeout(function () {
  $scope.value = "updated";
}, 1000);

Rules:

  • Do not call $apply() from an AngularJS event handler.
  • Avoid expensive watch expressions and deep watches on large data.
  • Watch specific properties instead of entire objects when possible.
  • Do not mutate a watched value indefinitely from its own listener.

FAQ

Does AngularJS use getters and setters for data binding?

No. AngularJS 1.x primarily uses dirty checking: it reevaluates watch expressions during digest cycles and compares their values.

What triggers an AngularJS digest cycle?

AngularJS directives and services commonly trigger it, including ng-click, ng-model, $http, $timeout, and $interval. External library callbacks may require $apply().

Why does my AngularJS view not update after setTimeout?

The browser's setTimeout is outside AngularJS. Use $timeout, or wrap the scope update in $scope.$apply().

What is the difference between $apply() and $digest()?

$apply() evaluates code and starts a root-level digest. $digest() checks only the current scope and its descendants. Application code generally uses $apply() only for external integration.

Does $watch detect nested object changes automatically?

Not by default. A standard watch normally compares object references. Watch a specific nested property, use $watchCollection for shallow collection changes, or use deep watching carefully.

Why can too many watchers hurt AngularJS performance?

AngularJS evaluates watchers during digest cycles. More watchers, deep comparisons, and expensive expressions increase the work required for each update.

Does AngularJS support Internet Explorer 6 and 7 because of watchers?

Its dirty-checking approach does not depend on modern property-observation features. Historical AngularJS browser support still depended on the specific AngularJS version and application requirements.

Take Quiz

Mini Project

Description

Build a small AngularJS status panel that updates a displayed connection state. It demonstrates the difference between an AngularJS-managed update and an update received from an external callback.

Goal

Display a connection status and safely refresh it when an external status event arrives.

Requirements

  • Create a controller with an initial connection status of Disconnected.
  • Display the status using an AngularJS template binding.
  • Add a button that changes the status through ng-click.
  • Simulate an external status callback that changes the status after a delay.
  • Ensure the external update is visible in the view.
Take Quiz

Keep learning

Related questions

View more