AngularJS
$httpBackend
asynchronous
web development
JavaScript

AngularJS httpBackend asynchronous response

Master System Design with Codemia

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

Introduction

In AngularJS tests, $httpBackend is not a real server that eventually answers on its own. It is a deterministic mock backend from ngMock, so the test controls when the response is released by calling $httpBackend.flush().

Understand the $httpBackend Model

The async part of $httpBackend testing is often misunderstood. Your application code still behaves as if it were waiting for HTTP, but in the test there is no real network delay unless you simulate it indirectly through the order in which you flush pending requests.

That means the normal testing flow is:

  1. define the expected request and response
  2. trigger the application code that makes the request
  3. call $httpBackend.flush()

Without the flush step, the mocked response stays pending and the promise chain does not complete.

The Basic Testing Pattern

Here is a small AngularJS service:

javascript
1app.service("UserService", function($http) {
2  this.loadUser = function() {
3    return $http.get("/api/user").then(function(response) {
4      return response.data;
5    });
6  };
7});

And the matching test:

javascript
1describe("UserService", function() {
2  var UserService, $httpBackend;
3
4  beforeEach(module("app"));
5
6  beforeEach(inject(function(_UserService_, _$httpBackend_) {
7    UserService = _UserService_;
8    $httpBackend = _$httpBackend_;
9  }));
10
11  afterEach(function() {
12    $httpBackend.verifyNoOutstandingExpectation();
13    $httpBackend.verifyNoOutstandingRequest();
14  });
15
16  it("returns mocked user data", function() {
17    var result;
18
19    $httpBackend.expectGET("/api/user").respond(200, { name: "Mark" });
20
21    UserService.loadUser().then(function(data) {
22      result = data;
23    });
24
25    $httpBackend.flush();
26
27    expect(result.name).toBe("Mark");
28  });
29});

The critical step is flush(). That is what releases the mocked response into the promise chain.

Why It Is Still Asynchronous in Structure

Even though the timing is deterministic, your code is still structured around asynchronous promises. That matters because the test is not bypassing the application's async behavior. It is just controlling exactly when the transport mock resolves.

This is the value of $httpBackend: your tests stay fast and predictable without relying on real network timing.

That predictability is also why $httpBackend was so widely used in AngularJS unit tests. It gave developers control over request expectations and resolution order without requiring a real HTTP server.

Multiple Requests and Partial Flushes

If your code makes several requests, you can flush them together or control how many pending responses are released.

javascript
1$httpBackend.expectGET("/api/user").respond(200, { id: 1 });
2$httpBackend.expectGET("/api/posts").respond(200, []);
3
4// trigger both requests in app code here
5
6$httpBackend.flush();

In more complex tests, flush(count) is useful when you want to release only part of the pending queue at a time. That lets you test intermediate application state between responses.

This is often the closest thing to simulating "asynchronous responses arriving later" in an AngularJS unit test. The responses are still controlled by the test, but you can release them in stages.

Promise Chains After the Response

Any .then(...) logic attached to the $http promise runs after the backend is flushed.

javascript
1app.service("UserService", function($http) {
2  this.loadUpperName = function() {
3    return $http.get("/api/user").then(function(response) {
4      return response.data.name.toUpperCase();
5    });
6  };
7});
8
9it("transforms the mocked response", function() {
10  var result;
11
12  $httpBackend.expectGET("/api/user").respond(200, { name: "Mark" });
13
14  UserService.loadUpperName().then(function(value) {
15    result = value;
16  });
17
18  $httpBackend.flush();
19
20  expect(result).toBe("MARK");
21});

So $httpBackend.flush() advances the transport mock, and the promise callbacks follow from there.

Digest Cycle Questions Can Still Appear

In many cases, flushing the backend is enough because AngularJS processes the $http promise resolution as part of that flow. But if your test also depends on other watcher-driven updates, you may still need digest-cycle handling in addition to the HTTP flush.

It helps to keep the two ideas separate:

  • flushing mocked HTTP work
  • propagating unrelated scope or watcher updates

If the test result depends on both, handle both deliberately instead of assuming the backend flush covers all state propagation automatically.

Keep the Verification Hooks

These cleanup checks are not optional decoration:

javascript
1afterEach(function() {
2  $httpBackend.verifyNoOutstandingExpectation();
3  $httpBackend.verifyNoOutstandingRequest();
4});

They catch tests that forgot to flush, expected the wrong URL, or left unresolved requests hanging around. Without them, a test suite can quietly pass while still hiding broken HTTP-test setup.

AngularJS Is Legacy Software Now

AngularJS is end-of-life, but many teams still maintain older codebases. In those projects, $httpBackend remains a valuable tool because it matches the original AngularJS testing model exactly.

If you are working in a newer Angular ecosystem, the tooling and testing patterns are different. This article is specifically about AngularJS and ngMock.

Common Pitfalls

One common mistake is forgetting $httpBackend.flush(), which leaves the promise unresolved and makes the result look undefined.

Another pitfall is treating $httpBackend as if it were a real time-delay simulator. It is a deterministic transport mock, not an autonomous server.

A third issue is mismatching the expected URL, method, or payload. In that case, the test fails because the mock setup is wrong, not because the application logic is wrong.

Finally, if the test depends on watcher-driven updates beyond the HTTP promise chain, flushing the backend alone may not be the whole story.

Summary

  • '$httpBackend responds only when the test calls flush().'
  • The application code remains asynchronous in structure, but the test controls the timing deterministically.
  • Define the expectation, trigger the request, then flush the backend.
  • Use verification helpers after each test to catch unresolved or mismatched requests.
  • '$httpBackend is the right mental model for AngularJS unit tests, but it is a mock backend, not a real server.'

Course illustration
Course illustration

All Rights Reserved.