HTTP communication is essential for modern web applications. This guide covers everything you need to know about handling HTTP requests in Angular, from callbacks and promises to observables and async/await patterns.

Table of Contents

What is HTTPClientModule?

HTTPClientModule provides several powerful features for handling HTTP requests in Angular:

Feature Description
Typed Responses Request and receive typed response objects
Error Handling Streamlined error handling and recovery
Testability Built-in features for testing HTTP requests
Interceptors Request and response interception and modification
Built-in Cache Intelligent caching of HTTP responses
Timeout Control Set timeouts for long-running requests

Synchronous vs Asynchronous Requests

Synchronous HTTP Requests

One task must be executed in a way that is dependent on another task - you must wait for the first task to complete before starting the next one. This creates a blocking pattern where operations are connected or dependent.

Characteristics:

  • Blocking: Code waits for execution to complete
  • Sequential: Operations happen one after another
  • Simpler logic but can freeze the UI

Asynchronous HTTP Requests

Tasks are totally independent and neither one considers the other in any way. Multiple operations can happen concurrently without waiting for each other.

Characteristics:

  • Non-blocking: Code continues without waiting
  • Concurrent: Multiple operations can run simultaneously
  • Better performance and improved UX
  • More complex to manage

Callbacks

A callback is a function that is passed into another function as an argument, which is then invoked inside the outer function to complete some kind of action or respond to an event.

Example:

js
function greeting(name) {
    alert("Hello " + name);
}

function processInput(callback) {
    var name = prompt("Please enter name", "Harry Potter");
    callback(name);
}

processInput(greeting);

Use Cases:

  • Event handlers
  • Asynchronous operations
  • Array methods (map, filter, forEach)

Drawback: Callback hell - deeply nested callbacks become difficult to read and maintain

Promises

A promise represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Promises allow you to attach success and failure handlers to async operations.

States:

State Description
Pending Initial state, operation hasn’t completed yet
Fulfilled Operation completed successfully
Rejected Operation failed

Once a promise is fulfilled with a value or rejected with a reason, the associated handlers attached via the then() method are invoked.

Example:

js
let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("Success!");
    }, 1000);
});

promise
    .then((result) => console.log(result))
    .catch((error) => console.error(error));

Async/Await

Async Functions

The async keyword placed before a function declaration makes that function an async function. It returns a promise automatically.

Example:

js
async function foo() {
    return "Hello World";
}

foo().then(console.log); // Output: Hello World

Await

The await keyword pauses execution until a promise settles and returns its result. It can only be used inside async functions.

Syntax:

js
let value = await promise;

Example:

js
async function foo() {
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => resolve("Hello World"), 1000);
    });

    let result = await promise;
    console.log(result); // Output: Hello World
}

foo();

Benefits:

  • Cleaner syntax than promise chains
  • Easier error handling with try/catch
  • More readable code flow

Key Note:

  • Promises use .then() method for handling results
  • Observables use .subscribe() method for handling values

Observables

Observables are a core feature of RxJS that allow you to work with streams of data. They’re particularly powerful in Angular for handling HTTP requests.

Ways to Subscribe to an Observable

1. Observer Object Pattern:

typescript
observable.subscribe({
    next: (value) => console.log(value),
    error: (error) => console.error(error),
    complete: () => console.log("Done"),
});

2. Separate Function Arguments:

typescript
observable.subscribe(
    (value) => console.log(value), // next
    (error) => console.error(error), // error
    () => console.log("Done") // complete
);

3. Arrow Functions:

typescript
observable.subscribe(
    (value) => console.log(value),
    (error) => console.error(error)
);

Key Observable Methods

typescript
// Subscribe with next handler only
observable.subscribe(nextFunc);

// Subscribe with next and error handlers
observable.subscribe(nextFunc, errorFunc);

// Subscribe with all handlers
observable.subscribe(nextFunc, errorFunc, completeFunc);

// Unsubscribe from observable
const subscription = observable.subscribe(nextFunc);
subscription.unsubscribe();

Error Handling

Promise Error Handling

1. Using .catch() method:

typescript
promise
    .then((data) => console.log(data))
    .catch((error) => console.error("Error:", error));

2. Using try/catch with async/await:

typescript
async function fetchData() {
    try {
        const response = await fetch("/api/data");
        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.error("Error:", error);
    }
}

Observable Error Handling

1. Using error callback:

typescript
observable.subscribe(
    (data) => console.log(data),
    (error) => console.error("Error:", error)
);

2. Using RxJS error operators:

typescript
import { catchError } from "rxjs/operators";

observable
    .pipe(
        catchError((error) => {
            console.error("Error:", error);
            return throwError("Error handled");
        })
    )
    .subscribe((data) => console.log(data));

Promises vs Observables

Both promises and observables handle asynchronous operations, but they have key differences:

Feature Promise Observable
Values Emitted Single value Multiple values
Execution Time Eager (immediate) Lazy (on subscribe)
Cancellable No Yes (unsubscribe)
Sharing Multicast Unicast
Handler Execution Asynchronous Synchronous
Subscription .then() .subscribe()
Best For Single async result Data streams

Single vs Multiple Values

  • Promise: Emits a single value
  • Observable: Can emit multiple values over time
typescript
// Promise - single value
const promise = Promise.resolve(42);
promise.then((value) => console.log(value)); // 42

// Observable - multiple values
const observable = interval(1000).pipe(take(3));
observable.subscribe((value) => console.log(value)); // 0, 1, 2

Eager vs Lazy

  • Promise: Eager - executor function runs immediately when promise is created
  • Observable: Lazy - executor function runs only when subscriber subscribes

Non-cancelable vs Cancelable

  • Promise: Cannot be cancelled once created
  • Observable: Can be cancelled via unsubscribe()
typescript
const subscription = observable.subscribe(nextFunc);
subscription.unsubscribe(); // Cancel the observable

Multicast vs Unicast

  • Promise: All .then() calls share the same execution
  • Observable: Each .subscribe() call gets its own execution

Async Handlers vs Sync Handlers

  • Promise: Handlers execute asynchronously (after all synchronous code)
  • Observable: Handlers execute synchronously (within the current execution flow)

Best Practices

Promise Best Practices

  1. Always handle rejections:

    typescript
    promise.catch((error) => console.error(error));
  2. Use async/await for cleaner code:

    typescript
    async function getData() {
        try {
            const data = await fetchData();
            return data;
        } catch (error) {
            console.error(error);
        }
    }

Observable Best Practices

  1. Always unsubscribe to prevent memory leaks:

    typescript
    const subscription = observable.subscribe(nextFunc);
    ngOnDestroy() {
        subscription.unsubscribe();
    }
  2. Use async pipe in templates:

    <div>{{ observable$ | async }}</div>
  3. Use RxJS operators for transformations:

    typescript
    observable
        .pipe(
            map((value) => value * 2),
            filter((value) => value > 10)
        )
        .subscribe(console.log);

HTTP Requests

For HTTP requests in Angular, prefer observables with proper error handling:

typescript
this.http
    .get("/api/data")
    .pipe(
        map((response) => response.data),
        catchError((error) => {
            console.error("HTTP Error:", error);
            return throwError("Failed to fetch data");
        })
    )
    .subscribe(
        (data) => console.log(data),
        (error) => console.error(error)
    );

Additional Resources