HttpClient: sending data and errors
To create a record on the server, you send a POST request with data in the body. Any request can fail: no network, or the server returned 404 or 500. The user should see both the loading state and a clear error message.
A POST request
Add a method to the PostApi service:
// src/app/post-api.ts
createPost(data: { title: string; body: string }) {
return this.http.post<Post>(this.url, { ...data, userId: 1 });
}The second argument of post() is the request body. HttpClient turns the object into JSON and sets the Content-Type: application/json header. put(), patch() and delete() work the same way.
Calling it from a component
ng generate component post-form// src/app/post-form/post-form.ts
import { Component, inject, signal } from '@angular/core';
import { PostApi } from '../post-api';
@Component({
selector: 'app-post-form',
templateUrl: './post-form.html',
})
export class PostForm {
private api = inject(PostApi);
message = signal('');
save(title: string, body: string) {
this.api.createPost({ title, body }).subscribe((post) => {
this.message.set(`Post created, id = ${post.id}`);
});
}
}<!-- src/app/post-form/post-form.html -->
<input #title placeholder="Title" />
<textarea #body placeholder="Text"></textarea>
<button (click)="save(title.value, body.value)">Publish</button>
<p>{{ message() }}</p>After the click, "Post created, id = 101" appears. JSONPlaceholder answers as if it saved the post, but in fact stores nothing — handy for learning.
Loading and errors
An object with next and error fields in subscribe() handles both outcomes:
// src/app/post-list/post-list.ts
import { Component, OnInit, inject, signal } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { PostApi } from '../post-api';
import { Post } from '../post';
@Component({
selector: 'app-post-list',
templateUrl: './post-list.html',
})
export class PostList implements OnInit {
private api = inject(PostApi);
posts = signal<Post[]>([]);
loading = signal(true);
error = signal('');
ngOnInit() {
this.api.getPosts().subscribe({
next: (posts) => {
this.posts.set(posts);
this.loading.set(false);
},
error: (err: HttpErrorResponse) => {
this.error.set(`Could not load posts (code ${err.status})`);
this.loading.set(false);
},
});
}
}<!-- src/app/post-list/post-list.html -->
@if (loading()) {
<p>Loading...</p>
} @else if (error()) {
<p class="error">{{ error() }}</p>
} @else {
@for (post of posts(); track post.id) {
<h3>{{ post.title }}</h3>
}
}To test the error, change the address in the service to a non-existent one, such as /postsx — "Could not load posts (code 404)" appears. Without a network the code is 0.
A fallback value with catchError
Sometimes it is better to handle the error in the service and return an empty list:
// src/app/post-api.ts
import { catchError, of } from 'rxjs';
getPostsSafe() {
return this.http.get<Post[]>(this.url).pipe(
catchError(() => of([])),
);
}The of([]) function creates an Observable with a single value — an empty array. The component receives [] and shows the @empty block.
Common mistake
Not handling the error at all. Then, on failure, the "Loading..." indicator hangs forever, and the error is visible only in the browser console. Every request whose result the user sees needs an error branch or a catchError operator.