Angular 37 🅰️ Error Handling
Errors are not exceptional. They are a normal part of every application that talks to a network, parses user input, or depends on code it does not control. A robust Angular application treats error handling as a design concern, not an afterthought. Errors appear at four distinct levels: within a component’s synchronous logic, within asynchronous operations (HTTP calls, timers, promises), within the template rendering, and at the boundary between the application and the global error handler. Each level has its own mechanism — try/catch, RxJS catchError, Angular’s ErrorHandler, and the global error listeners — and a complete strategy uses all four. This chapter covers each level, how they interact, the patterns that keep errors from becoming silent failures, and the design decisions that determine whether an error surfaces to the user, is logged for developers, or both. It builds on Angular 35 and 36, where HttpClient and interceptors were introduced, and it treats error handling as the discipline that makes those tools reliable in production.
Key point: Angular has a built-in ErrorHandler that catches errors from templates, lifecycle hooks, and event handlers. The default implementation logs to the console, but it can be replaced with a custom handler that reports to a logging service. For asynchronous operations, RxJS’s catchError is the tool: it intercepts an error notification, transforms it, and either recovers or rethrows. The HttpErrorResponse from HttpClient carries the HTTP status and the server’s error body, and it is the object to inspect when a request fails. For user-facing errors, the right pattern is to distinguish recoverable from unrecoverable, show feedback for the former, and log both.
The four levels of error handling
Angular errors do not all arrive at the same place. Knowing which level an error belongs to determines the mechanism that catches it.
Synchronous errors occur in the component’s own code — a function that throws, a value that is null when it should not be. They are caught with try/catch if the code anticipates them, or they propagate to the ErrorHandler if they do not.
Asynchronous errors occur in Observables, Promises, and timers. For Observables, the error channel of the subscription receives them, and catchError transforms them. For Promises, .catch() or await in a try/catch handles them. Unhandled asynchronous errors reach the global error handler.
Template errors occur during change detection — a binding expression that throws, a pipe that fails, a directive that errors. Angular’s change detection catches these and forwards them to the ErrorHandler. They do not crash the application, but they break the view.
Global errors are everything that escapes the above. The ErrorHandler catches most of them, and browser-level listeners (window.onerror, window.onunhandledrejection) catch the rest. This is where logging and reporting live.
Why the levels matter. Each level has a different mechanism and a different audience. Synchronous errors are usually bugs and should be fixed. Asynchronous errors are often recoverable and should be handled gracefully. Template errors are usually data-shape problems and should be guarded against. Global errors are the safety net, and they exist so no error goes unnoticed.
Why Angular’s default
ErrorHandleris not enough for production. The default implementation logs the error to the console and rethrows it. In a development build, the rethrow is useful because it surfaces the error in the console. In a production build, the error reaches the browser’s console but goes nowhere else — no log aggregation, no alerting, no record. A customErrorHandlerthat sends errors to a logging service is the minimum for production.
The ErrorHandler service
Angular provides an ErrorHandler class that is the default handler for errors from templates, lifecycle hooks, and event bindings. It can be replaced with a custom implementation.
import { ErrorHandler, Injectable, inject } from '@angular/core';
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
private readonly logger = inject(LoggingService);
handleError(error: unknown): void {
this.logger.error(error);
// Do not rethrow in production — the application should continue running.
}
}
The custom handler receives the error, sends it to the logging service, and does not rethrow. Rethrowing is the default behavior, and it is useful in development because it surfaces the error in the console. In production, rethrowing has no benefit and can disrupt the application.
Why a custom handler is registered in the providers. The ErrorHandler is a service, and Angular uses whichever implementation is registered for the token. To replace the default, provide the custom class for the ErrorHandler token in the application’s providers.
export const appConfig: ApplicationConfig = {
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
],
};
The useClass provider replaces the default. From that point on, every error that Angular catches goes to the custom handler.
What the handler catches and what it does not. It catches errors from component templates, lifecycle hooks, event bindings, and setTimeout callbacks that Angular wraps (which is most of them). It does not catch errors thrown in a Promise that is not awaited, errors in code that runs outside Angular’s zone (in zoneless applications), or errors in the browser’s own APIs. For those, window.onerror and window.onunhandledrejection are the fallback.
Why logging and user feedback are separate concerns. The ErrorHandler is a developer-facing mechanism — it logs and reports. It should not show a toast or a modal to the user, because it does not know the context. A user-facing error message depends on what the user was doing, and that context is available in the component or the interceptor, not in the global handler. The pattern is: the handler logs, the component or interceptor shows feedback.
Why the handler should be careful about what it logs. The error object may contain user data, tokens, or other sensitive information. A logging service should sanitize before sending. Logging the raw error in development is fine; logging it in production without review is a risk.
Handling errors in observables
Most asynchronous work in Angular flows through Observables. The catchError operator is the tool for handling errors in a stream.
getUser(id: string): Observable<User> {
return this.http.get<User>(`/api/users/${id}`).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 404) {
return of(null); // recover with a null value
}
return throwError(() => error); // rethrow for other statuses
}),
);
}
The catchError operator intercepts the error notification, and the callback returns a new Observable. If it returns of(null), the stream recovers and emits null as a normal value. If it returns throwError(() => error), the error continues down the stream.
Why the distinction between recovery and rethrow matters. Recovery means the caller receives a normal value and does not see the error. Rethrow means the caller’s error callback fires. The choice depends on whether the error is expected and recoverable. A 404 for a user lookup might be recoverable — the caller can treat it as “user not found.” A 500 is not recoverable — the caller should know the request failed.
Why catchError must return an Observable. The operator’s callback must return an Observable, not a value. of(value) produces a single-value Observable that emits the value and completes. throwError(() => error) produces an Observable that emits the error and completes with the error notification. Returning a value directly is a type error.
The retry operator. retry resubscribes to a failed Observable a fixed number of times before giving up.
this.http.get('/api/data').pipe(
retry({ count: 3, delay: 1000 }),
catchError(this.handleError),
);
The retry operator resubscribes after a delay, up to the given count. If all attempts fail, the error propagates to catchError. This is the standard pattern for transient failures — a network blip, a momentary timeout — and it is most appropriate for idempotent requests.
The retryWhen and retry with delay function. For exponential backoff, the delay option can be a function.
retry({ count: 3, delay: (attempt) => Math.pow(2, attempt) * 1000 })
The first retry waits 2 seconds, the second 4, the third 8. Exponential backoff reduces load on a struggling server and is the standard pattern for retry logic.
Why retry and catchError are often combined. retry handles transient failures; catchError handles the final failure after retries are exhausted. Together they form the complete handling for an HTTP call: retry a few times, and if it still fails, transform the error into something the caller can work with.
HTTP errors in detail
HttpClient reports failures through the HttpErrorResponse class. Its properties carry the information needed to handle the error correctly.
| Property | Description |
|---|---|
status | HTTP status code (0 for network errors) |
statusText | Status message |
error | Response body or error object |
message | Human-readable message |
url | The request URL |
headers | Response headers |
name | The error class name |
The status property is the primary discriminator. A status of 0 means the request never reached the server or the response never came back — a network error, a CORS failure, or a timeout. Status 4xx means the request was understood but rejected. Status 5xx means the server failed.
Why the status is the first thing to check. The handling differs by status. A 401 may trigger a token refresh. A 403 should inform the user they lack permission. A 404 may be handled as a normal absence. A 500 should be logged and shown as a generic failure. The status is the key to the branch.
Why the error property is often a structured object. When the server returns a JSON body with an error, HttpErrorResponse.error contains that object. A validation failure often comes back as { errors: { email: "already taken" } }, which the form can display. Reading error.error and mapping it to the form is a standard pattern.
Why the message property is not for display. The message is a developer-facing description like “Http failure response for /api/users: 500 Internal Server Error.” It is not suitable for showing to a user. A user-facing message depends on the context and should be derived from the status and the error body.
Why a network error is status 0. The fetch API reports network failures without a status because there was no HTTP response. A CORS preflight failure, a DNS failure, or a disconnected network all produce status 0. The error property in this case is a ProgressEvent rather than a server response, which is worth knowing when inspecting the error object.
User-facing error messages
The message the user sees is a separate concern from the error that was caught. A component that handles an error should decide what the user needs to know.
Why a generic message is often better than the raw error. A user cannot act on “Http failure response for /api/users: 500.” A message like “Could not load users. Please try again.” tells the user what happened in their terms and what to do. The technical details belong in the log, not on the screen.
Why the message should be specific when the error is specific. A 404 on a user profile should say “User not found,” not “Something went wrong.” A validation error should point at the field that failed. The general rule is: be specific when the user can act, be generic when they cannot.
Why the error state belongs in the component. The component knows what the user was doing and what the user needs. It should hold the error state — a signal or a property — and the template should render it. The global handler logs; the component displays.
@Component({
template: `
@if (error()) {
<div class="error">
{{ error() }}
<button (click)="retry()">Retry</button>
</div>
}
`,
})
export class UserListComponent {
private readonly userService = inject(UserService);
readonly users = signal<User[]>([]);
readonly error = signal<string | null>(null);
load(): void {
this.error.set(null);
this.userService.getUsers().subscribe({
next: (users) => this.users.set(users),
error: (err: HttpErrorResponse) => this.error.set(this.messageFor(err)),
});
}
private messageFor(err: HttpErrorResponse): string {
if (err.status === 0) return 'Check your connection and try again.';
if (err.status === 404) return 'No users found.';
if (err.status >= 500) return 'Server error. Please try again later.';
return 'Could not load users.';
}
}
The component holds the error, renders it, and offers a retry. The message is chosen by status, and the raw error is not shown.
Why the retry button is a good pattern. When an error is recoverable, offering the user a way to retry is better than requiring a page reload. The button calls the same load method, and the error state is cleared before the attempt.
Global error listeners
Some errors do not go through Angular’s ErrorHandler. Promise rejections that are not awaited, errors in code that runs outside Angular’s zone, and errors in third-party libraries that do not use Angular’s error handling all escape. For these, the browser’s global listeners are the fallback.
@Injectable({ providedIn: 'root' })
export class GlobalErrorListener {
private readonly logger = inject(LoggingService);
constructor() {
window.addEventListener('error', (event) => {
this.logger.error(event.error ?? event.message);
});
window.addEventListener('unhandledrejection', (event) => {
this.logger.error(event.reason);
});
}
}
The error event fires for uncaught exceptions. The unhandledrejection event fires for Promise rejections that were not handled. Both are routed to the logging service. This service is instantiated at application startup, and from then on it captures everything the ErrorHandler does not.
Why the global listeners are a safety net, not the primary mechanism. They catch everything that escapes, but they have no context — they do not know what the user was doing or which component failed. They are for logging and alerting, not for user-facing handling. The ErrorHandler and the per-operation handling cover the cases where context matters.
Why the ordering of registration matters. The global listeners should be registered before any application code runs, so no error is missed. In a standalone application, the service can be injected in the root component’s constructor, or registered in a provideAppInitializer or APP_INITIALIZER. The point is to have the listeners in place before the application starts doing work.
Why the listeners should not throw. The handlers for the global events must not themselves throw, or the error handling breaks. They should log and return. If the logging itself fails, it should fail silently — an error in the error handler is worse than the original error.
Why Angular’s zone affects this. In a zone-based application, Angular wraps asynchronous operations and forwards errors to the
ErrorHandler. In a zoneless application, that wrapping does not exist, and more errors escape to the global listeners. The zoneless model makes the global listeners more important, not less, which is why the pattern is worth having in both cases.
Complete Example Session
import { Component, ErrorHandler, Injectable, inject, signal, ApplicationConfig } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { catchError, retry, throwError, of } from 'rxjs';
import { Observable } from 'rxjs';
// ============================================
// PART 1: LOGGING SERVICE
// ============================================
@Injectable({ providedIn: 'root' })
export class LoggingService {
private readonly entries = signal<{ message: string; timestamp: number }[]>([]);
error(error: unknown): void {
const message = error instanceof Error ? error.message : String(error);
this.entries.update((list) => [...list, { message, timestamp: Date.now() }]);
console.error('[Global]', error);
}
}
// ============================================
// PART 2: CUSTOM ERROR HANDLER
// ============================================
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
private readonly logger = inject(LoggingService);
handleError(error: unknown): void {
this.logger.error(error);
}
}
// ============================================
// PART 3: GLOBAL LISTENERS
// ============================================
@Injectable({ providedIn: 'root' })
export class GlobalErrorListener {
private readonly logger = inject(LoggingService);
constructor() {
window.addEventListener('error', (event) => {
this.logger.error(event.error ?? event.message);
});
window.addEventListener('unhandledrejection', (event) => {
this.logger.error(event.reason);
});
}
}
// ============================================
// PART 4: SERVICE WITH ERROR HANDLING
// ============================================
export interface User {
id: string;
name: string;
}
@Injectable({ providedIn: 'root' })
export class UserService {
private readonly http = inject(HttpClient);
getUser(id: string): Observable<User | null> {
return this.http.get<User>(`/api/users/${id}`).pipe(
retry({ count: 2, delay: 1000 }),
catchError((error: HttpErrorResponse) => {
if (error.status === 404) {
return of(null); // recover — user not found is a valid outcome
}
return throwError(() => error); // rethrow — caller needs to know
}),
);
}
getUsers(): Observable<User[]> {
return this.http.get<User[]>('/api/users').pipe(
catchError((error: HttpErrorResponse) => throwError(() => error)),
);
}
}
// ============================================
// PART 5: COMPONENT WITH ERROR STATE
// ============================================
@Component({
selector: 'app-user-detail',
standalone: true,
template: `
@if (loading()) {
<p>Loading…</p>
} @else if (error()) {
<div class="error">
<p>{{ error() }}</p>
<button (click)="load()">Retry</button>
</div>
} @else if (user()) {
<h1>{{ user()!.name }}</h1>
}
`,
})
export class UserDetailComponent {
private readonly userService = inject(UserService);
readonly user = signal<User | null>(null);
readonly loading = signal(false);
readonly error = signal<string | null>(null);
load(): void {
this.loading.set(true);
this.error.set(null);
this.userService.getUser('u1').subscribe({
next: (user) => {
this.user.set(user);
this.loading.set(false);
},
error: (err: HttpErrorResponse) => {
this.error.set(this.messageFor(err));
this.loading.set(false);
},
});
}
private messageFor(err: HttpErrorResponse): string {
if (err.status === 0) return 'Check your connection and try again.';
if (err.status === 403) return 'You do not have permission to view this user.';
if (err.status >= 500) return 'Server error. Please try again later.';
return 'Could not load the user.';
}
}
// ============================================
// PART 6: APPLICATION CONFIGURATION
// ============================================
export const appConfig: ApplicationConfig = {
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
// GlobalErrorListener is injected in the root component to install listeners
],
};
The example shows the four levels working together: a custom ErrorHandler for Angular-caught errors, global listeners for escaped errors, catchError and retry for HTTP errors, and a component that holds error state and shows a message.
Quick Reference
Error Handling Levels
| Level | Mechanism | Catches |
|---|---|---|
| Synchronous | try/catch | Thrown in own code |
| Observable | catchError | Errors in streams |
| Template | ErrorHandler | Binding and lifecycle errors |
| Global | window.onerror | Everything else |
catchError Patterns
| Pattern | Effect |
|---|---|
of(fallback) | Recover with a value |
throwError(() => e) | Rethrow |
EMPTY | Complete without emitting |
of(fallback) + side effect | Recover and notify |
HttpErrorResponse Status
| Status | Meaning | Typical Handling |
|---|---|---|
0 | Network/CORS | Connection message |
400 | Bad request | Show validation |
401 | Unauthorized | Refresh or redirect |
403 | Forbidden | Permission message |
404 | Not found | Treat as absence |
500 | Server error | Log, generic message |
503 | Unavailable | Retry |
Retry Operators
| Operator | Purpose |
|---|---|
retry(n) | Retry n times |
retry({ count, delay }) | Retry with delay |
retryWhen | Custom retry logic |
ErrorHandler Registration
| Step | Code |
|---|---|
| Define | class GlobalErrorHandler implements ErrorHandler |
| Register | { provide: ErrorHandler, useClass: GlobalErrorHandler } |
| Dependencies | inject() in the class |
| Rethrow | Not in production |
Global Listeners
| Event | Fires For |
|---|---|
error | Uncaught exceptions |
unhandledrejection | Unhandled Promise rejections |
Best Practices
✅ Do This:
// Register a custom ErrorHandler in production
{ provide: ErrorHandler, useClass: GlobalErrorHandler } // ✅
// Use catchError to recover or rethrow
catchError((e: HttpErrorResponse) => e.status === 404 ? of(null) : throwError(() => e)) // ✅
// Retry only idempotent requests
retry({ count: 2, delay: 1000 }) // ✅
// Hold error state in the component
readonly error = signal<string | null>(null); // ✅
// Translate errors into user-facing messages
if (err.status === 0) return 'Check your connection.'; // ✅
// Log the raw error, show the friendly one
this.logger.error(err); this.error.set(this.messageFor(err)); // ✅
// Offer a retry action when recoverable
<button (click)="load()">Retry</button> // ✅
❌ Don’t Do This:
// Don't show the raw error message to the user
this.error.set(err.message); // "Http failure response..." // ⚠️
// Don't swallow errors silently
catchError(() => EMPTY); // user sees nothing // ⚠️
// Don't retry non-idempotent requests
retry(3); // on a POST may duplicate // ⚠️
// Don't rethrow in a production ErrorHandler
handleError(error) { throw error; } // default behavior, no log // ⚠️
// Don't log sensitive data
this.logger.error(JSON.stringify(user)); // may contain tokens // ⚠️
// Don't assume the ErrorHandler catches everything
// Promise rejections and zone-external errors escape // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Raw error shown to user | Confusing message | Map status to message |
| Error swallowed | Silent failure | Rethrow or set error state |
catchError returns a value | Type error | Return of(value) |
Retry on POST | Duplicates | Only retry GET |
| ErrorHandler rethrows | Unhandled in production | Log and return |
| Promise rejection escapes | Not caught | Global listener |
| Error state not cleared | Stale message | Clear before retry |
retry without delay | Hammers the server | Use delay |
| Error body not inspected | Missed validation detail | Read error.error |
| Logging tokens | Security leak | Sanitize before logging |
Real-World Examples
1. 404 as absence
catchError((e: HttpErrorResponse) => e.status === 404 ? of(null) : throwError(() => e))
2. Retry with backoff
retry({ count: 3, delay: (attempt) => Math.pow(2, attempt) * 1000 })
3. Global handler
{ provide: ErrorHandler, useClass: GlobalErrorHandler }
4. Global listener
window.addEventListener('unhandledrejection', (e) => logger.error(e.reason));
5. Validation errors
if (e.status === 400) form.setErrors(e.error.errors);
6. 401 with refresh
catchError((e: HttpErrorResponse) => e.status === 401
? auth.refresh().pipe(switchMap(() => next(req)))
: throwError(() => e));
7. User-facing message
private messageFor(err: HttpErrorResponse): string { ... }
8. Error state signal
readonly error = signal<string | null>(null);
9. Retry button
<button (click)="load()">Retry</button>
10. Logging without leaking
this.logger.error({ status: err.status, url: err.url });
Visual: The Four Levels
┌──────────────────────────────────────────────────────────┐
│ SYNCHRONOUS │
│ try { risky(); } catch (e) { handle(e); } │
│ Scope: one function │
│ │
├──────────────────────────────────────────────────────────┤
│ OBSERVABLE │
│ source.pipe(catchError(handler)) │
│ Scope: one stream │
│ │
├──────────────────────────────────────────────────────────┤
│ ANGULAR ErrorHandler │
│ handleError(error) { logger.error(error); } │
│ Scope: templates, lifecycle, event bindings │
│ │
├──────────────────────────────────────────────────────────┤
│ GLOBAL │
│ window.onerror / unhandledrejection │
│ Scope: everything that escapes │
│ │
│ Each level catches what the previous one missed. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: catchError Flow
┌──────────────────────────────────────────────────────────┐
│ http.get('/api/users') │
│ │ │
│ ▼ │
│ retry({ count: 2 }) │
│ │ │
│ ├── attempt 1 fails ──► wait ──► attempt 2 │
│ │ │
│ └── attempt 3 fails ──► error propagates │
│ │ │
│ ▼ │
│ catchError(handler) │
│ │ │
│ ├── recoverable ──► of(fallback) ──► next: value │
│ │ │
│ └── unrecoverable ──► throwError ──► error: caller │
│ │
└──────────────────────────────────────────────────────────┘
Visual: HttpErrorResponse Status Branches
┌──────────────────────────────────────────────────────────┐
│ HttpErrorResponse │
│ │ │
│ ├── status === 0 ──► network / CORS │
│ │ │
│ ├── status 400 ──► validation errors │
│ │ │
│ ├── status 401 ──► refresh token or redirect │
│ │ │
│ ├── status 403 ──► permission denied message │
│ │ │
│ ├── status 404 ──► treat as absence │
│ │ │
│ ├── status 5xx ──► log, generic message │
│ │ │
│ └── other ──► generic message │
│ │
└──────────────────────────────────────────────────────────┘
Visual: User-Facing vs Developer Logging
┌──────────────────────────────────────────────────────────┐
│ ERROR │
│ │ │
│ ├── USER SEES │
│ │ "Could not load users. Please try again." │
│ │ - specific when actionable │
│ │ - generic when not │
│ │ - never the raw message │
│ │ │
│ └── DEVELOPER SEES │
│ status: 500 │
│ url: /api/users │
│ error: { message: "DB connection lost" } │
│ stack: ... │
│ - logged, aggregated, alerted │
│ - never shown to the user │
│ │
└──────────────────────────────────────────────────────────┘
Visual: Error Handling Decision
┌──────────────────────────────────────────────────────────┐
│ Where did the error occur? │
│ │ │
│ ├── In a synchronous function │
│ │ └── try / catch │
│ │ │
│ ├── In an Observable │
│ │ └── catchError (recover or rethrow) │
│ │ │
│ ├── In a template binding │
│ │ └── ErrorHandler (log) + guard in template │
│ │ │
│ ├── In a Promise │
│ │ └── .catch() or await in try/catch │
│ │ │
│ └── Somewhere else │
│ └── Global listeners │
│ │
│ Then decide: log it, show it, or both. │
│ │
└──────────────────────────────────────────────────────────┘
Summary
| Level | Mechanism | Purpose |
|---|---|---|
| Synchronous | try/catch | Local handling |
| Observable | catchError, retry | Stream handling |
| Template/Lifecycle | ErrorHandler | Angular-caught errors |
| Global | window.onerror | Escaped errors |
| HTTP | HttpErrorResponse | Status-based handling |
| User feedback | Component state | Contextual message |
| Developer feedback | Logging service | Diagnosis and alerting |
Key takeaways:
- Errors appear at four levels — synchronous, observable, template/lifecycle, and global — and each level has its own mechanism
- The
ErrorHandleris the Angular-level catch — replace it with a custom implementation to log and report errors from templates, lifecycle hooks, and event bindings catchErroris the observable-level tool — recover withof(value)or rethrow withthrowError(() => error), depending on whether the error is recoverableretryhandles transient failures — it resubscribes a fixed number of times with an optional delay, and it should only be used for idempotent requestsHttpErrorResponsecarries the status — a status of0is a network error,4xxis a client error,5xxis a server error, and each is handled differently- User-facing messages are separate from the error — the component holds the error state and shows a message derived from the status, never the raw error
- The
ErrorHandlerlogs; the component displays — the handler has no context for a user-facing message, and the component has no business logging to the aggregation service - Global listeners catch what escapes —
window.onerrorandwindow.onunhandledrejectionare the safety net for errors that bypass Angular’s zone - Errors should never be silent — a swallowed error is worse than a visible one, because it hides the failure and delays the fix
- Retry with backoff, not tight loops — exponential delay reduces load on a struggling server and is the standard pattern
Remember: Error handling in Angular is a stack, not a single mechanism. The synchronous try/catch handles the local case, catchError handles the stream, the ErrorHandler handles what Angular catches, and the global listeners handle the rest. The user-facing message is a separate decision from the error that was caught, and the logging is a separate decision from both. Getting all three right — catch, log, and show — is what makes an application reliable when things go wrong, which they will.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!