Debugging • Testing • Forms and Routing • Build and Deploy • Owning Features • 2026

Angular Interview Questions for 3 Years Experience (2 to 4 Years)

Angular interviews for 3 years of experience ask less for definitions and more for what you did, such as an error you traced, a form or route that misbehaved, how you test components and services, how you debug and deploy, and what code review taught you. This page is written for Angular developers with about two to four years of real work, the stage where you own features end to end but someone else still designs the app. Each question shows what the interviewer is checking, the shape of a good answer and a short spoken answer. Swap the stories for your own before the interview.

Search all questions by round, difficulty and level, or save the ones you want to practice.

Debugging 4 questions

Medium Behavioral round Mid-level Practice question

1. Tell me about an ExpressionChangedAfterItHasBeenCheckedError you hit in your own code. What was actually wrong, and how did you fix it?

What the interviewer is really testing:
Whether you understand the error as a data-flow problem, state changing while Angular is rendering, rather than a warning to silence with setTimeout.
Answer frame:

Read it: the message gives the old and new value; find the binding that shows them.

Find the write: which code changed that value during the check, often a child hook pushing state up.

Fix the flow: give the value one owner so nothing updates it mid-render, instead of hiding it with setTimeout.

Sample spoken answer:

“On an edit page, the Save button in the header was bound to a formValid flag in the parent. The form itself lived in a child component, and that child emitted its valid status to the parent from ngOnInit. The button sits above the child in the template, so Angular had already checked it with the old value when the child changed it. In development mode Angular checks twice and caught the difference, and the error showed false becoming true. My first instinct was a setTimeout, but a reviewer pointed out that just hides it. The real fix was to let the parent own the FormGroup and pass it down, so the button reads form.invalid directly and there's one source of truth. The error went away and the code got simpler too.”

Red flag to avoid:

Saying you fixed it with setTimeout or detectChanges without being able to explain what changed the value during the check.

They may ask next:
  • Why does this error only show up in development mode?
  • When is calling detectChanges a reasonable fix, and when is it hiding a problem?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

2. Errors in production were only showing up in users' browser consoles, so nobody saw them. How would you capture them in an Angular app?

What the interviewer is really testing:
Whether you know Angular's ErrorHandler hook and the practical details of error reporting: context, noise, and readable stack traces.
Answer frame:

Hook: a class that implements ErrorHandler, provided in place of the default one.

Context: send route, app version and a user id, not just the message.

Make it usable: private source maps for readable traces, and dedupe so one loop does not flood the log.

Sample spoken answer:

“Angular sends uncaught errors to its ErrorHandler, which by default just logs to the console. I wrote our own class that implements ErrorHandler and provided it instead. It still logs to the console, but it also sends the error to our logging endpoint with the current route, the app version and the user id, so we can tell who hit it and on which build. HTTP errors that a component already handles don't reach it, which is fine, those are expected. Two lessons from doing it. Production stack traces are minified, so we upload source maps privately to the logging tool instead of serving them publicly. And one error in a loop sent thousands of reports, so now it drops repeats of the same message within a short window.”

Code:
@Injectable()
export class AppErrorHandler implements ErrorHandler {
  private log = inject(ErrorLogService);

  handleError(error: unknown) {
    console.error(error);
    this.log.report(error); // adds route, version, user id; drops repeats
  }
}

// app config
providers: [{ provide: ErrorHandler, useClass: AppErrorHandler }]
Red flag to avoid:

Wrapping every method in try and catch, or shipping public source maps without thinking about it.

They may ask next:
  • Which errors will never reach the ErrorHandler?
  • What would you not send to the log, for privacy reasons?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

3. Some users see a due date one day earlier than the one saved. The API sends dates like 2024-03-10, and your service turns them into Date objects before the template shows them. What's wrong?

What the interviewer is really testing:
Whether you have dealt with time zones in a real app and know how a date-only string gets parsed.
Answer frame:

Cause: new Date on a date-only string gives midnight UTC, which is shown in the browser's local time.

Who sees it: users west of UTC get the previous day; sending back with toISOString can shift the other way.

Fix: treat date-only values as plain dates, not instants, both when showing and when saving.

Sample spoken answer:

“Our service mapped every response with new Date on the due date. A string like 2024-03-10 with no time is read by JavaScript as midnight UTC, and the date pipe then formats that moment in the browser's time zone. For anyone behind UTC, midnight UTC is still the evening of the ninth, which is why only some users saw it. Saving had the reverse bug: we built a Date at local midnight and sent it with toISOString, which converts to UTC and can land on the previous day for users ahead of UTC. The fix was agreeing that a due date is a date, not a moment in time. We keep it as the plain string end to end, and when we need a Date for a date picker we build it from the year, month and day parts, which gives local midnight.”

Code:
// new Date('2024-03-10') is midnight UTC, not local midnight
function toLocalDate(value: string): Date {
  const [y, m, d] = value.split('-').map(Number);
  return new Date(y, m - 1, d);
}
Red flag to avoid:

Adding or subtracting a day to make it look right on your own machine.

They may ask next:
  • How would you test this if your own laptop is in a time zone where the bug does not show?
  • When should a value be stored as a full timestamp instead of a date?
Say it in 60 seconds
Easy Technical round Mid-level Practice question

4. Which tools do you use day to day to debug an Angular app, and what do you use each one for?

What the interviewer is really testing:
Whether you debug with tools, breakpoints and the component inspector, not only console logs.
Answer frame:

Angular DevTools: component tree, current inputs and state, and the profiler for change detection.

Browser tools: breakpoints in TypeScript through source maps, network tab, console.

Dev-mode helpers: ng.getComponent on a selected element to look at a live component.

Sample spoken answer:

“Most days it's the browser devtools plus the Angular DevTools extension. In the component tree I can click a component and see its current inputs and properties, which answers most "why is this showing that" questions quickly. When a page feels slow, I use the profiler tab to see how many change detection runs happen and which components take the time. For logic bugs I set breakpoints in the Sources panel. Source maps mean I'm stepping through my TypeScript, not the built code. The network tab tells me whether the bug is in the request, the response or my code. And in development mode I sometimes select an element and call ng.getComponent on it in the console to look at the live component. I still use console.log, but only for quick checks.”

Red flag to avoid:

Relying only on console.log and never having used breakpoints or the component inspector.

They may ask next:
  • How would you find out why a component re-renders more often than you expect?
  • How do you debug something that only happens in the production build?
Say it in 60 seconds

Routing 2 questions

Medium Technical round Mid-level Practice question

5. On a product page, a link to a related product changes the URL from /products/1 to /products/2, but the page still shows product 1. Why, and how did you fix it?

What the interviewer is really testing:
Whether you know the router reuses the component when only a parameter changes, so code that reads the id once in ngOnInit goes stale.
Answer frame:

Cause: same route, new param, so the router keeps the component and ngOnInit does not run again.

Bug: the id was read once from the route snapshot.

Fix: react to paramMap with switchMap, or bind the param to a component input.

Sample spoken answer:

“The router sees the same route with a different parameter, so it reuses the existing component instead of creating a new one. That means ngOnInit doesn't run again. Our code read the id once from route.snapshot.paramMap in ngOnInit, so the URL changed but nothing reloaded. I changed it to listen to route.paramMap and switchMap into the API call, so every new id fetches its product and a slow earlier request gets cancelled instead of overwriting the newer one. Another clean option is turning on component input binding in the router, so the id arrives as an input and updates when the URL changes. While I was in there I also reset the scroll position and the selected tab, because those would have stayed from product 1 as well.”

Code:
private route = inject(ActivatedRoute);
private api = inject(ProductApi);

product$ = this.route.paramMap.pipe(
  map(params => params.get('id')!),
  switchMap(id => this.api.getProduct(id))
);
Red flag to avoid:

Suggesting a full page reload or forcing the router to recreate the component instead of reacting to the parameter.

They may ask next:
  • Why switchMap here and not mergeMap?
  • What other state on the page would stay behind from the previous product?
Say it in 60 seconds
Medium Coding round Mid-level Practice question

6. Users complained that a list page forgets its filters and page number when they press back or refresh. How would you keep that state in the URL?

What the interviewer is really testing:
Whether you can use query parameters as the source of truth for list state, and handle the details like history entries and parsing.
Answer frame:

Write: on filter change, navigate to the same route with new queryParams, merging the rest.

Read: build the list request from queryParamMap, so refresh, back and shared links all work.

Details: replaceUrl while typing, parse numbers and defaults, reset the page when a filter changes.

Sample spoken answer:

“I made the URL the single place the filters live. When a user changes a filter, I navigate to the same route with the new query params and merge them with the ones already there. The list doesn't listen to the form directly. It listens to queryParamMap, parses the values with defaults, and fetches from there. That way refresh, back and a link pasted in chat all show the same list. Two details bit me. Typing in the search box was adding a history entry per keystroke, so I used replaceUrl for text search and debounced it. And when someone changed a filter on page five, they got an empty page, so any filter change now resets page to one.”

Code:
applyFilters(status: string, page = 1) {
  this.router.navigate([], {
    relativeTo: this.route,
    queryParams: { status: status || null, page },
    queryParamsHandling: 'merge',
  });
}

orders$ = this.route.queryParamMap.pipe(
  map(p => ({ status: p.get('status') ?? '', page: Number(p.get('page') ?? 1) })),
  switchMap(f => this.api.getOrders(f))
);
Red flag to avoid:

Keeping the filters only in a service or local storage, so a shared link or the back button still shows the wrong list.

They may ask next:
  • What does setting a query param to null do when you merge?
  • How would you keep the filter form in sync when the user presses back?
Say it in 60 seconds

RxJS at Work 2 questions

Hard Technical round Mid-level Practice question

7. In the network tab you notice the same order request firing three times when one page loads. The template uses the async pipe in a few places. What is happening?

What the interviewer is really testing:
Whether you know HttpClient observables are cold, so every subscriber runs its own request, and how to share one result.
Answer frame:

Cause: each async pipe is its own subscription, and each subscription to an HttpClient observable sends a request.

Simplest fix: subscribe once in the template with @if and an alias, then use that value.

Shared stream: shareReplay with bufferSize 1 and refCount, or turn it into a signal once.

Sample spoken answer:

“HttpClient observables are cold. Nothing happens until someone subscribes, and every subscriber gets its own request. Our template used the async pipe on the same order stream in the header, in the summary and in a child input, so three subscriptions meant three calls. The quickest fix was to subscribe once at the top with @if and an alias called order, and use that everywhere below. Where a stream really had to be shared across components, I added shareReplay with bufferSize one and refCount true, so late subscribers get the last value without a new request. Converting it once with toSignal works too. I also check the network tab on every page I build now, because this kind of duplication doesn't show up any other way.”

Code:
order$ = this.route.paramMap.pipe(
  switchMap(p => this.api.getOrder(p.get('id')!)),
  shareReplay({ bufferSize: 1, refCount: true })
);
Red flag to avoid:

Blaming Angular or the backend, or adding a cache in the service without understanding why three subscriptions meant three requests.

They may ask next:
  • What does refCount change, and when would leaving it off cause a leak?
  • Why does putting shareReplay on a stream that depends on a route param need care?
Say it in 60 seconds
Hard Situational round Mid-level Practice question

8. QA reports that after one user logs out and another logs in on the same browser, the second user briefly sees the first user's data. What is going on, and how do you fix it?

What the interviewer is really testing:
Whether you see that services in root keep their state across logout in a single-page app, and treat it as a security bug.
Answer frame:

Cause: root services live for the whole app, so cached subjects, signals and HTTP caches survive logout.

Treat it as serious: one user seeing another's data is a privacy bug, not a cosmetic flash.

Fix: one logout path that clears every store, or a full page reload on logout; then test the swap.

Sample spoken answer:

“In a single-page app, logging out doesn't reload anything. Every service provided in root keeps living, so a BehaviorSubject holding the last loaded orders, or a small cache in an interceptor, still has the first user's data when the second one lands on the page. It shows until the new request comes back. I'd treat this as a privacy bug and fix it today, not as a flicker. The quick, reliable fix is to do a full page load on logout, which throws away all in-memory state. The cleaner fix, which we did next, was one logout function that tells every store to reset, and keying caches by user id so stale data can never match. Then I added a test that logs in as one user, logs out, logs in as another and checks nothing leaks.”

Red flag to avoid:

Calling it a cosmetic flicker and hiding it with a loading spinner instead of clearing the state.

They may ask next:
  • How would you make sure a new store someone adds next year also gets cleared?
  • What else in the browser could still hold the first user's data?
Say it in 60 seconds

Forms 3 questions

Medium Technical round Mid-level Practice question

9. You load an existing record into a reactive form for editing, and a valueChanges listener wipes one of the fields you just loaded. How do you load the data safely?

What the interviewer is really testing:
Whether you know setValue versus patchValue, and that setting values in code fires valueChanges unless you tell it not to.
Answer frame:

setValue vs patchValue: setValue needs every control and throws if one is missing; patchValue sets whatever matches.

Side effects: patching fires valueChanges, so a listener like "country changed, clear city" runs on load too.

Fix: patch with emitEvent false, or order the load so dependent lists are ready first.

Sample spoken answer:

“We had an address form where changing the country clears the city and loads a new city list. When I loaded a saved address with patchValue, the country change fired valueChanges, and the listener cleared the city I had just set. So the form opened with an empty city. I changed the load to patchValue with emitEvent false, then loaded the city list for that country myself. I use patchValue for loading because the API object often has extra or missing fields, while setValue throws if a control is missing, which I sometimes use on purpose when I want the shape checked. One thing I didn't need was markAsPristine. Setting values from code doesn't make a form dirty, only user input does.”

Code:
this.form.patchValue(address, { emitEvent: false });
this.loadCities(address.country);
Red flag to avoid:

Fixing it with a boolean like isLoading checked inside every listener, or not knowing that setting values from code fires valueChanges.

They may ask next:
  • What else stops working when you pass emitEvent false?
  • How would you warn the user about unsaved changes on this form?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

10. A field that shows on the form is missing from the object your form sends to the API. The field is disabled. Why, and what do you do about it?

What the interviewer is really testing:
Whether you know form.value leaves out disabled controls, and the right way to disable a control in a reactive form.
Answer frame:

Cause: form.value skips disabled controls; getRawValue includes them.

Decide: should the server get this field at all, or is it display only?

Disable properly: call disable() or create the control disabled, not the HTML disabled attribute.

Sample spoken answer:

“In a reactive form, disabled controls are left out of form.value. We had a customer code field that was disabled once the record existed, and the update call stopped sending it, which the API rejected. getRawValue returns everything, disabled controls included, so that's what I used for the save. But I also asked whether the field should go to the server at all. If the user can't change it, maybe it belongs in the URL or the record id, not the form body. The other thing I cleaned up was how it was disabled. Someone had put the disabled attribute on the input next to formControlName, and Angular warns about that in the console. The right way is control.disable() or creating the control disabled from the start.”

Code:
code = new FormControl({ value: 'C-104', disabled: true });

save() {
  const body = this.form.getRawValue(); // includes disabled controls
  this.api.update(this.id, body).subscribe();
}
Red flag to avoid:

Using the disabled attribute in the template on a reactive control, or copying the value in by hand without knowing why it was missing.

They may ask next:
  • How does a disabled control affect the validity of the whole form?
  • When would readonly be a better choice than disabled?
Say it in 60 seconds
Medium Coding round Mid-level Practice question

11. Build the form for an invoice where the user can add and remove line items, each with a product and a quantity. How do you set it up, and how do you load an existing invoice?

What the interviewer is really testing:
Whether you have really used FormArray: building rows, adding and removing them, and the trap that patchValue does not create missing rows.
Answer frame:

Shape: a FormGroup with a FormArray of small groups, one per line.

Add and remove: push a new group, removeAt by index; buttons get type button so they do not submit.

Load: clear the array and push one group per saved line, because patchValue will not add rows.

Sample spoken answer:

“I keep a FormGroup with a FormArray called items, and a small method that builds one line group with its validators. Add pushes a new group, remove calls removeAt with the index. In the template I use formArrayName and loop over the controls with formGroupName set to the index. The Add and Remove buttons need type button, or they submit the form, which I learned the hard way. The trap is editing. When I first loaded a saved invoice with three lines into a form that had one row, patchValue filled the first row and quietly ignored the other two. It doesn't create controls. So to load, I clear the array, push one group per saved line, and then patch the rest of the form.”

Code:
private fb = inject(NonNullableFormBuilder);
form = this.fb.group({
  customer: ['', Validators.required],
  items: this.fb.array([this.newItem()]),
});
get items() { return this.form.controls.items; }

newItem(product = '', qty = 1) {
  return this.fb.group({
    product: [product, Validators.required],
    qty: [qty, [Validators.required, Validators.min(1)]],
  });
}
addItem() { this.items.push(this.newItem()); }
removeItem(i: number) { this.items.removeAt(i); }

load(inv: Invoice) {
  this.items.clear();
  inv.items.forEach(l => this.items.push(this.newItem(l.product, l.qty)));
  this.form.patchValue({ customer: inv.customer });
}
Red flag to avoid:

Expecting patchValue or setValue to create the missing rows, or not knowing why a button inside the form submits it.

They may ask next:
  • How would you show a running total that updates as quantities change?
  • How would you require at least one line item before the form is valid?
Say it in 60 seconds

Components at Work 2 questions

Medium Technical round Mid-level Practice question

12. You try to focus an input using @ViewChild, but the reference is undefined. The input is inside a panel shown with @if. What is going on?

What the interviewer is really testing:
Whether you know when view queries are filled in and how conditional blocks affect them.
Answer frame:

Timing: view queries are ready in ngAfterViewInit, not ngOnInit or the constructor.

Conditional: inside @if the element does not exist until the condition is true and the view updates.

Fix: react when the query changes, with a ViewChild setter or the viewChild signal query.

Sample spoken answer:

“Two things were going on. First, view queries aren't filled in until the view is created, so reading them in ngOnInit gives undefined. They're ready in ngAfterViewInit. Second, our input lived inside an @if, so even in ngAfterViewInit it was undefined while the panel was closed. When the user opened the panel, the code set the flag and tried to focus straight away, but the input only exists after Angular renders that change. The fix I used was a setter on @ViewChild, which runs whenever the element appears, and I focus it there. With newer Angular I'd use the viewChild signal query, which updates when the element shows up. static true doesn't help here, because it only works for elements that aren't inside a conditional block.”

Code:
@ViewChild('nameInput') set nameInput(el: ElementRef<HTMLInputElement> | undefined) {
  el?.nativeElement.focus();
}
Red flag to avoid:

Wrapping the focus call in a setTimeout until it works, without knowing when the element actually exists.

They may ask next:
  • What is the difference between ViewChild and ContentChild?
  • When would you use static true, and what does it change?
Say it in 60 seconds
Medium Situational round Mid-level Practice question

13. Your ticket needs a change to a shared table component that about fifteen pages use, and none of them are yours. How do you make the change without breaking the others?

What the interviewer is really testing:
Whether you change shared code carefully: backwards compatible inputs, checking every use, and telling the people who depend on it.
Answer frame:

Additive change: a new optional input with a default that keeps today's behaviour.

Check the uses: search every place it is used, run their tests, look at a few pages by hand.

Tell people: a short note to the owners, and never a check for one page name inside the shared component.

Sample spoken answer:

“First I'd look for a way to make the change additive. If my page needs sortable columns, I add an optional sortable input that defaults to false, so every existing page behaves exactly as today. I'd avoid anything like checking which page it's on inside the shared component, because that turns it into a pile of special cases. Then I search every place the component is used and run the tests for those areas, and I click through a few of the busiest pages myself. I'd also add a test in the shared component for the old behaviour, so my change can't quietly alter it. Before merging, I'd post a short note in the team channel saying what changed and that the default is unchanged, and tag whoever owns it.”

Red flag to avoid:

Changing the default behaviour because it suits your page, or copying the component to make your own version without telling anyone.

They may ask next:
  • What would you do if the change could not be made backwards compatible?
  • How do you decide between adding an input and letting pages pass their own template?
Say it in 60 seconds

Testing 3 questions

Medium Coding round Mid-level Practice question

14. How do you unit test a component that loads orders from a service in ngOnInit and shows them in a table? Show me the test.

What the interviewer is really testing:
Whether you actually write component tests with TestBed: replacing the real service, triggering change detection and checking the rendered output.
Answer frame:

Replace the service: provide a fake with useValue that returns of(...) with test data.

Render: create the component and call detectChanges, which runs ngOnInit and the template.

Check output: query the DOM for what the user would see, not private fields.

Sample spoken answer:

“I don't want the real service or the network in a component test, so I provide a fake for the service with useValue, returning an observable of a couple of test orders. Then I create the component with TestBed and call fixture.detectChanges, which runs ngOnInit and renders the template. After that I check the DOM, for example that there are two rows and the first shows the right total, because that's what the user sees. I also add a test for the empty case and the error case, since those are where bugs hide. One thing that caught me once: if the component lists the service in its own providers array, the TestBed provider is ignored, and I have to use overrideComponent instead.”

Code:
it('shows one row per order', () => {
  const fakeService = {
    getOrders: () => of([{ id: 1, total: 20 }, { id: 2, total: 35 }]),
  };
  TestBed.configureTestingModule({
    imports: [OrderListComponent],
    providers: [{ provide: OrderService, useValue: fakeService }],
  });
  const fixture = TestBed.createComponent(OrderListComponent);
  fixture.detectChanges();

  const rows = fixture.nativeElement.querySelectorAll('tr.order-row');
  expect(rows.length).toBe(2);
});
Red flag to avoid:

Testing only private fields and method calls, or letting component tests hit the real backend.

They may ask next:
  • What does a NullInjectorError in a test usually tell you?
  • How would you test that clicking a row navigates to the order page?
Say it in 60 seconds
Medium Coding round Mid-level Practice question

15. How do you test a service that calls an API with HttpClient, including what happens when the server returns an error?

What the interviewer is really testing:
Whether you know the HTTP testing tools: expecting a request, checking it, answering it with data or an error, and verifying nothing else was sent.
Answer frame:

Setup: provideHttpClient then provideHttpClientTesting, and inject HttpTestingController.

Expect and answer: expectOne the URL, check method or body, flush data or an error status.

Verify: call verify after each test so an extra or missing request fails it.

Sample spoken answer:

“I set up TestBed with provideHttpClient and then provideHttpClientTesting, in that order, and inject both the service and the HttpTestingController. In the test I call the service method and subscribe, then use expectOne with the URL. That fails if the request wasn't made, or was made twice. I check the method and, for a post, the body. Then I flush fake data and assert what the subscriber got. For errors I flush with a status like 500 and check that the service maps it to the message or fallback we expect. In afterEach I call verify, which fails the test if any request was left unanswered. That's caught real bugs for me, like a service that quietly fired a second request.”

Code:
beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [provideHttpClient(), provideHttpClientTesting()],
  });
  service = TestBed.inject(OrderService);
  http = TestBed.inject(HttpTestingController);
});
afterEach(() => http.verify());

it('loads orders for a customer', () => {
  let result: Order[] | undefined;
  service.getOrders(7).subscribe(o => (result = o));

  const req = http.expectOne('/api/customers/7/orders');
  expect(req.request.method).toBe('GET');
  req.flush([{ id: 1, total: 20 }]);
  expect(result?.length).toBe(1);
});
Red flag to avoid:

Mocking HttpClient by hand with a fake get method, so the test never checks the URL, the method or the error path.

They may ask next:
  • How would you test that a failed request is retried once before giving up?
  • Where do you draw the line between this test and an end-to-end test?
Say it in 60 seconds
Hard Technical round Mid-level Practice question

16. Your search component waits 300 milliseconds after typing before it calls the API. How do you test that without making the test slow or flaky?

What the interviewer is really testing:
Whether you can control time in tests with fakeAsync and tick, and know its limits.
Answer frame:

Fake time: wrap the test in fakeAsync so timers only move when you call tick.

Prove the wait: type, tick just under the delay, expect no call; tick the rest, expect exactly one.

Limits: fakeAsync needs zone.js in the test setup, and leftover timers fail the test.

Sample spoken answer:

“I wrap the test in fakeAsync, so timers don't run on their own and only move forward when I call tick. I provide a fake API that records the terms it was called with. Then I type into the input and dispatch an input event, type again, and tick 299 milliseconds. At that point I expect no calls. One more tick and I expect exactly one call, with the last term. That proves both the debounce and that the first keystroke got dropped. Two things to know. If a timer is still pending when the test ends, fakeAsync fails it, so I use flush or make sure everything settles. And fakeAsync depends on zone.js, so in a project without zone.js in the tests I'd use the test runner's fake timers instead.”

Code:
it('searches once after typing stops', fakeAsync(() => {
  const calls: string[] = [];
  const fakeApi = { search: (t: string) => { calls.push(t); return of([]); } };
  TestBed.configureTestingModule({
    imports: [SearchComponent],
    providers: [{ provide: SearchApi, useValue: fakeApi }],
  });
  const fixture = TestBed.createComponent(SearchComponent);
  fixture.detectChanges();
  const input: HTMLInputElement = fixture.nativeElement.querySelector('input');

  input.value = 'ang'; input.dispatchEvent(new Event('input'));
  input.value = 'angular'; input.dispatchEvent(new Event('input'));
  tick(299);
  expect(calls).toEqual([]);
  tick(1);
  expect(calls).toEqual(['angular']);
}));
Red flag to avoid:

Making the test wait for real time, or only checking that the API was called without proving the delay.

They may ask next:
  • What is the difference between tick and flush?
  • Why is a real wait with setTimeout in a test a bad idea?
Say it in 60 seconds

Build and Deploy 3 questions

Medium Technical round Mid-level Practice question

17. The app works locally, but deployed under example.com/portal/ it shows a blank page and the console says the script files return 404. What do you check?

What the interviewer is really testing:
Whether you understand base href and how the built index.html finds its scripts and assets.
Answer frame:

Cause: index.html has base href set to the site root, so it asks for scripts at /main.js, not /portal/main.js.

Fix: build with the right base href, from the command line or the build config.

Assets: use relative paths, because a path starting with a slash ignores the base href.

Sample spoken answer:

“The first thing I'd open is the built index.html and look at the base tag. By default it's a single slash, so the browser asks for main.js at the site root, and under /portal/ that's a 404, which gives you a blank page. The fix is building with base href set to /portal/, either with the flag on ng build or in the build configuration so nobody forgets it. The router also uses the base href, so links become /portal/orders on their own. Then I'd check images and icons. We had a few written as /assets/logo.png, and a leading slash goes to the domain root and ignores the base href, so I changed them to relative paths.”

Code:
ng build --base-href /portal/
Red flag to avoid:

Guessing at server settings without first opening the built index.html and the failing request URLs.

They may ask next:
  • What else does the server need so refreshing /portal/orders works?
  • How would you build the same app once and deploy it under different paths?
Say it in 60 seconds
Hard Technical round Mid-level Practice question

18. After you added a feature, the build started warning that the main bundle is over budget and the first load got slower. How do you find out what grew?

What the interviewer is really testing:
Whether you can measure a bundle instead of guessing, and know the usual causes of a jump in the initial bundle.
Answer frame:

Measure: build with stats and open them in a bundle analyzer to see what is in the initial chunk.

Usual causes: a whole library imported for one function, or an eager import pulling a lazy feature into main.

Fix and guard: import only what you need, move heavy code behind a lazy route or @defer, keep the budget.

Sample spoken answer:

“The budget warning was the good part, it caught it before users complained. I built with stats turned on and opened the output in a bundle analyzer, which shows each package by size inside each chunk. Two things stood out. I'd imported a date helper from a big utility library as a default import, which pulled in the whole library instead of one function. And a chart component I'd added to a shared barrel file meant the dashboard's chart library was now in the initial bundle, even though only one lazy page used it. I switched to importing the single function, stopped exporting the chart from the shared barrel, and wrapped the chart in @defer. The main bundle went back under budget, and I added the analyzer check to my own routine for new libraries.”

Red flag to avoid:

Raising the budget limit so the warning goes away, or guessing at causes without looking at what is in the bundle.

They may ask next:
  • What is the difference between the initial bundle and a lazy chunk, and which one matters for first load?
  • How can a barrel file with index.ts exports pull code into the main bundle?
Say it in 60 seconds
Easy Technical round Mid-level Practice question

19. How do you handle different API URLs and settings for development, staging and production in your Angular app? Where do secrets go?

What the interviewer is really testing:
Whether you know how environment files work at build time, and that anything in the frontend bundle is public.
Answer frame:

Build time: environment files swapped by fileReplacements per build configuration.

Public: everything in the bundle can be read by anyone, so no secrets there.

Runtime option: load a small config file at startup when one build must go to many environments.

Sample spoken answer:

“We keep an environment file per setup with things like the API base URL and feature flags, and the build configuration swaps them in with fileReplacements. So ng build with the production configuration bakes in the production file. The rule I stick to is that nothing secret goes there. Whatever's in an environment file ends up in the JavaScript anyone can open in the browser. Keys that are meant to be public, like a maps key locked to our domain, are fine. Anything else stays on the server, and the frontend calls our backend instead. On one project, ops wanted to build once and deploy the same files to staging and production, so we switched to loading a config.json at app startup and reading the URL from there.”

Red flag to avoid:

Putting a private API key or a password in an environment file because it is not committed as plain text in the component.

They may ask next:
  • What goes wrong if one build is deployed to several environments with environment files?
  • How would you make sure the app does not start before the runtime config has loaded?
Say it in 60 seconds

Owning Features 3 questions

Easy Behavioral round Mid-level Practice question

20. Tell me about code review feedback on your Angular code that changed how you write components.

What the interviewer is really testing:
Whether you take review well and can explain the reason behind a change, not just that you made it.
Answer frame:

The comment: what the reviewer flagged, in one line.

The why: what you learned about how Angular works.

The habit: what you do differently now, with a small example.

Sample spoken answer:

“Early on I had a table where each row called methods from the template, like isOverdue(order) for a CSS class and getTotal(order) in a cell. A senior dev left a comment asking how often I thought those ran. I guessed once per row. The real answer is on every change detection pass, so every click or keystroke anywhere on the page ran them again for every row. It wasn't slow yet, but getTotal looped over line items, so it would be with bigger orders. We moved that work out of the template: the total and the overdue flag were worked out once when the data arrived. In newer code I use computed signals or a pure pipe for this. Now when I write a function call in a template, I stop and ask how often it'll run.”

Red flag to avoid:

Describing the change without being able to say why it mattered, or getting defensive about the comment.

They may ask next:
  • Is calling a method in a template always a problem?
  • Tell me about feedback you disagreed with. What did you do?
Say it in 60 seconds
Medium Behavioral round Mid-level Practice question

21. Walk me through an Angular feature you owned from the ticket to production. What did you decide yourself, and what would you do differently?

What the interviewer is really testing:
Whether you really owned the work: clarifying the ticket, the API contract, the states people forget, tests and the release.
Answer frame:

The ask: what the feature was for and what you clarified before coding.

Your decisions: component split, state, API contract, loading, empty and error states, tests.

Release and after: how it shipped, what broke or surprised you, what you would change.

Sample spoken answer:

“I owned a bulk import screen where support staff upload a spreadsheet of orders. Before coding, I asked what should happen with bad rows, and we agreed to show a preview with errors per row and only import the valid ones. I agreed the response shape with the backend developer early, so I could build against a mocked service while the API was being written. I split it into an upload component, a preview table and a results summary, with the state in one service. I made sure the loading, empty and partly-failed states all had designs. I wrote component tests for the preview and service tests for the upload. It shipped behind a feature flag to two users first, and they found that files with a header row in a different order broke it. Next time I'd ask for real sample files on day one.”

Red flag to avoid:

Only describing the code, with no word on clarifying the ask, testing or what happened after the release.

They may ask next:
  • How did you know the feature was working once it was live?
  • Which part would you split into its own ticket if you did it again?
Say it in 60 seconds
Medium Behavioral round Mid-level Practice question

22. Tell me about an Angular feature where your estimate was far off. Which parts did you leave out, and how do you plan work now?

What the interviewer is really testing:
Whether you learn from a miss and now estimate the parts that are easy to forget, not just the happy path.
Answer frame:

The miss: the task, your estimate and how long it really took.

What you missed: the specific parts, not just "it was harder".

Now: how you break work down and when you raise a slip.

Sample spoken answer:

“I estimated a date range filter for a reports page at two days. It took almost two weeks. The picker itself was quick. What I missed was everything around it. The dates had to survive in the URL, the backend expected a different time zone handling than I assumed, the empty state for a range with no data had no design, and the picker had keyboard problems that QA flagged. I also didn't say anything until day four, which made it worse. Now I break a task into the component, the state and the URL, the API contract, the states nobody designed, and tests, and I estimate each part. If the first unknown takes longer than expected, I tell my lead that day, not days later.”

Red flag to avoid:

Blaming the ticket or other people, or saying you still estimate the same way.

They may ask next:
  • How do you estimate a task that depends on an API that does not exist yet?
  • What do you do when you are asked for an estimate on the spot?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

You practiced these. On the real call, ClapAssist helps with the rest.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your live interview audio and screen are never stored. Your resume and notes are saved to your account so the app fills them in on any computer. It stays out of screen share on every plan, including Free; only you can see it.

Download with 10 free minutes
Mac and Windows · Stays out of screen share · No card