DEV Community

Cover image for Fixing Angular NG02100 & [object Object] Issues by Sanitizing API Responses
Md. Injamul Alam
Md. Injamul Alam

Posted on

Fixing Angular NG02100 & [object Object] Issues by Sanitizing API Responses

Learn how to handle unexpected empty objects ({}) in API responses cleanly in Angular before they break Date pipes or ruin your UI tables.

When building enterprise applications like CashBooks or Accounting Ledgers, you often deal with large JSON objects containing optional properties. A common issue arises when backend APIs return empty objects {} instead of null or undefined for empty fields.

In Angular, this leads to two major issues:

  1. NG02100: InvalidPipeArgument: The built-in DatePipe cannot parse {} into a valid date.
  2. [object Object] cluttering the UI: Stringified empty objects appear as text inside HTML table cells.

In this quick guide, we’ll look at why this happens and how to handle it efficiently at the data-fetching layer without cluttering your HTML templates.

The Problem

Consider the following JSON response from a financial service endpoint:

[
  {
    "ID": 12,
    "drTransDate": "2026-09-12T00:00:00",
    "drAccountName": "Cash-In-Hand",
    "drCash": 1000,
    "crTransDate": {},
    "crAccountName": {},
    "crCash": {}
  }
]
Enter fullscreen mode Exit fullscreen mode

In your Angular template:

<td>{{ data.crTransDate | date: 'dd-MMM-yyyy' }}</td>
<td>{{ data.crAccountName }}</td>
Enter fullscreen mode Exit fullscreen mode

What happens under the hood?

  1. {} is truthy in JavaScript, so default checks like data.crTransDate ? ... : '' still evaluate to true.
  2. Passing {} to Angular's DatePipe triggers RuntimeError: NG02100: InvalidPipeArgument.
  3. Standard properties display literal [object Object] on the screen.

The Solution: RxJS Data Sanitization

Instead of adding heavy conditionals (typeof === 'object') into every single column in your HTML template, sanitize the incoming array when consuming the RxJS observable.

Here is the clean implementation inside your Angular Component:

import { Component, ChangeDetectorRef } from '@angular/core';

@Component({
  selector: 'app-cashbook',
  templateUrl: './cashbook.component.html'
})
export class CashBookComponent {
  getList: any[] = [];
  btnLoading = false;
  loadingForList = false;

  constructor(
    private reportsService: ReportsService,
    private cdr: ChangeDetectorRef
  ) {}

  fetchCashBookData(searchObj: any) {
    this.btnLoading = true;
    this.loadingForList = true;

    this.reportsService.getCashBookList(searchObj).subscribe({
      next: (res: any) => {
        this.btnLoading = false;
        this.loadingForList = false;

        // Cleanse API data: Convert empty `{}` into `null`
        this.getList = (res?.results || []).map((item: any) => {
          const cleanItem: any = {};
          for (const key in item) {
            if (
              item[key] &&
              typeof item[key] === 'object' &&
              Object.keys(item[key]).length === 0
            ) {
              cleanItem[key] = null;
            } else {
              cleanItem[key] = item[key];
            }
          }
          return cleanItem;
        });

        this.cdr.markForCheck();
      },
      error: (err) => {
        this.btnLoading = false;
        this.loadingForList = false;
        console.error('Failed to load data', err);
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Why This Approach Works Best

  1. Cleaner Templates: Your HTML remains simple without complex nested ternary checks:
   <td>{{ data?.crTransDate | date: 'dd-MMM-yyyy' }}</td>
   <td>{{ data?.crAccountName || '' }}</td>
Enter fullscreen mode Exit fullscreen mode
  1. Pipe Safe: Angular pipes ignore null or undefined values gracefully without throwing runtime crashes.
  2. Performance First: The loop runs once when data is received, preventing recalculations during Angular change detection cycles.

Alternative: RxJS map Operator

If you prefer keeping your subscriptions clean, you can shift this transformation logic directly into an RxJS pipe operator inside your service:

import { map } from 'rxjs/operators';

getCashBookList(searchObj: any) {
  return this.http.post<any[]>(this.apiUrl, searchObj).pipe(
    map((res: any) =>
      (res?.results || []).map((item: any) =>
        Object.fromEntries(
          Object.entries(item).map(([k, v]) => [
            k,
            v && typeof v === 'object' && Object.keys(v).length === 0 ? null : v
          ])
        )
      )
    )
  );
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Data sanitization at the boundary layer (Service or Component Subscription) is essential when dealing with unpredictable or legacy APIs. Converting truthy empty objects ({}) to null ensures your Angular UI stays stable, performant, and bug-free.

How do you handle data transformations in your Angular apps? Let's discuss in the comments below!

Top comments (2)

Collapse
 
raknaos profile image
Raknaos •

The truthiness of {} is the part these pipelines keep forgetting: data.crTransDate ? ... : '' reads like a guard and isn't one, so the crash lands two layers later inside the pipe instead of at the response that caused it. Two edges in the cleansing loop itself are worth knowing about before it ships. It walks one level, so an empty object nested inside an array — lines: [{}] — survives untouched and comes back as the same NG02100 three columns over. And the predicate is "no own enumerable keys", not "empty": a Date instance, or a class with only getters, also has zero own keys and gets silently replaced with null.

Doing it in an HttpInterceptor rather than per component changes what you learn from it, too. Once every response is normalised in one place, the fields arriving as {} stop being a rendering bug and become a report you can read — that's how I found the endpoints where "no credit side on this line" and "the caller didn't ask for the credit side" had collapsed into the same empty object. The null that saves your template is also the one erasing the difference. Does the sanitiser cover array items too where you use it, or only the top-level fields?

Collapse
 
injamulcse15 profile image
Md. Injamul Alam •

Thanks for the detailed and insightful feedback! You hit the nail on the head regarding the {} truthiness issue and those subtle edge cases.
Currently, the sanitizer only checks top-level fields, which is why nested array objects like lines: [{}] bypass it. Handling array items and preserving object instances like Date without unintended null replacements is definitely something I need to refine. Moving this logic into an HttpInterceptor to standardize response payloads at the network layer also makes total sense to keep the templates clean.
Appreciate you pointing these edge cases out—really helpful!