Skip to main content
New tool CRON Expression Builder — preview next run times before you schedule Apex. Open the builder →
Interview prep

LWC interview questions and answers

Lifecycle hooks, the wire service, events and re-render traps.

13 questions · 4 of them scenarios

Junior round

Junior LWC questions

LWC junior scenario

A component renders an empty card with no error. How do you debug it?

Testing Where you look when there is no error message to read.

What they tell you

A component wired to an Apex method shows its heading and nothing else. No error appears on screen, the browser console is clean, and the same Apex method returns rows when run in Anonymous Apex.

junior

An empty render with a clean console usually means the wire returned an error and the template has nowhere to put it. So the first thing I would do is render the error: log both halves of the wire result, or add an if:true={error} block to the template. If the error turns out to be an access problem, that also explains why Anonymous Apex works — that runs as me, and the component runs as whoever is looking at it.

mid

Working in Anonymous Apex and returning nothing in the component is the most useful clue in the report, because Anonymous Apex runs as an administrator in system mode. That points at permissions or sharing rather than the query. I would surface the error first — a wire always gives me data or error, and a template that only checks data swallows the second half silently — then read what it says. If it is INSUFFICIENT_ACCESS, the fix is a permission set, not code. And whatever the cause, the error branch stays in the template, because the next person to hit this deserves a message rather than an empty card.

Ask before you answer
  • Does the template guard on data alone, or does it also render the error branch when one comes back?
  • Is the Apex method running as the current user, and does that user have read access to the object?
Do not say this

Add console.log statements through the JavaScript until something shows up.

The JavaScript is the least likely place for this. A wire that failed populates error, not data, and a template that only renders when data is truthy will show exactly this — a heading and nothing else — with no console output at all. The missing piece is an error branch, not a log.

LWC junior

Do you still need @track, and what is @api for?

Testing Which era of LWC you learned. This one dates an answer immediately.

junior

Not usually. Since the framework made all fields reactive, assigning to a field re-renders on its own, so @track is only relevant when I change something inside an object or an array without reassigning it. @api is different — it makes a property or a method public, so a parent can set it in markup or call it. The habit worth having is to reassign rather than mutate, and then reactivity is not something I think about.

mid

@track used to be required on everything and now it is required almost nowhere, which is why an answer that leans on it tells you when someone learned this. Fields are reactive by default; what is not reactive is a mutation the framework cannot see, so this.rows.push(row) will not re-render while this.rows = [...this.rows, row] will. @track on that field makes the mutation visible again, and I would still rather reassign, because the immutable version is also what stops a child from editing a parent's array by accident. @api is a different contract entirely — it is the public surface, one-way from parent to child, and mutating an @api property inside the child is an error rather than a style preference.

Deep dive on this
LWC junior

When do you call Apex with @wire and when do you call it imperatively?

Testing Whether you have hit the case where a wire will not do what you need, or only used the one from the tutorial.

junior

A wire is for reading data the component displays. It runs on its own, re-runs when a reactive parameter changes, and hands me data or error. Imperative is for anything I trigger — a button, a save, a search — because I need to decide when it runs and get a promise back. A wire also needs the Apex method marked cacheable=true, and a method that writes cannot be cacheable, so anything with DML in it is imperative by definition.

mid

The dividing line is who decides when the call happens. @wire is declarative and cached: I give it $recordId and it re-invokes when that changes, which is exactly right for a component that shows a record's related data and nothing else. It is also the source of two surprises — I cannot call it on demand, and its cached result does not refresh after my own DML unless I hold the wire result and pass it to refreshApex. Imperative calls give me control and a promise, at the cost of writing the loading and error states myself. In practice I wire the reads and call the writes, then refresh the wire after a write rather than maintaining two copies of the same data.

Deep dive on this
Mid-level round

Mid-level LWC questions

LWC mid

Lightning Data Service or your own Apex — how do you choose?

Testing That the choice has a security dimension, not only a convenience one.

mid

Lightning Data Service first, every time it can do the job. getRecord, updateRecord and createRecord from lightning/uiRecordApi enforce sharing, object and field access without me writing a line of it, they share a cache with every other component on the page, and a save in one component updates the others for free. Apex is for what LDS cannot express: more than one object in a call, an aggregate, a query with real conditions, or logic that has to run server side. The cost of choosing Apex is that every one of those security checks becomes mine to write.

senior

I treat it as a security decision that happens to be convenient. An @AuraEnabled method is a new public entry point into the org, and it is only as safe as the WITH USER_MODE or stripInaccessible I remember to put in it — LDS has no equivalent hole because the UI API is doing the enforcement above my code. So the question I ask is not "which is easier" but "am I adding an entry point, and does it need to exist". Where LDS genuinely runs out is worth knowing precisely rather than guessing: some standard objects are still not supported by the UI API, and nothing in LDS does an aggregate or a multi-object query, so those are Apex and that is fine. What I would not do is write Apex for a single-record read because a lightning-record-form looked too plain — that is trading away sharing enforcement and a shared cache for styling I could do with record-edit-form instead.

Deep dive on this
LWC mid

Which lifecycle hook do you put a call in, and why not renderedCallback?

Testing What you have broken by choosing the wrong one. Everyone knows the list; fewer know which one bites.

mid

connectedCallback for anything that should happen once when the component appears — an imperative Apex call, a subscription, reading a URL parameter. Not the constructor, because the element is not in the DOM yet and this.template queries return nothing. And not renderedCallback, because it runs after every render: if the call sets a field, that field triggers a render, and the render triggers the callback again. That loop is the classic way to fire 200 Apex calls in a few seconds.

senior

The order matters as much as the choice. Parents connect before children, children render before the parent's renderedCallback, which is why renderedCallback is the only place a parent can reliably reach into a child's rendered DOM — and also why it is the place people put logic that then re-runs forever. When I genuinely need it, it gets a guard: a boolean field set the first time, so the body runs once. errorCallback is the one worth knowing precisely, because it is narrower than it sounds: it catches errors thrown in the lifecycle hooks and render of descendant components, and it does not catch an error in my own event handler or in a rejected promise. So a component that looks defended by errorCallback can still fail silently in the handler where most errors actually happen, and that needs its own catch.

LWC mid scenario

A list does not update after the user saves a record, but a page refresh shows it. What is happening?

Testing The difference between the framework's cache and your own data. This is the most reported non-bug in Lightning.

What they tell you

A component wires an Apex method to show a list of Contacts. A second component on the same page creates a Contact using createRecord. The new Contact appears after the browser is refreshed and not before.

mid

The wire is being served from cache, and nothing has told it the underlying data changed. createRecord notifies Lightning Data Service, so an LDS adapter on the same record would update — but an Apex wire is outside that cache entirely, so it has no idea. The fix is to refresh it deliberately: keep the wire result in a field, and after the save call refreshApex(this.wiredContacts). If the two components are unrelated, the save also has to tell the list it happened, which is a message channel rather than an event.

senior

Two problems stacked, and the caching one is the shallower of them. The immediate fix is refreshApex on the held wire result, triggered after the save resolves. The structural question is how the list finds out at all: if the components are siblings in one parent, an event upward and a property downward is enough; if they are in separate regions on a Lightning page, nothing connects them and Lightning Message Service is the honest answer. Where I would push further is on whether the Apex wire needs to exist. A getListUi or getRelatedListRecords adapter shares the LDS cache, so the save invalidates it automatically and this class of bug does not arise — that is worth more than the query flexibility for a plain list. And whatever the mechanism, the refresh belongs after the promise resolves rather than next to the call, or it races the save it is meant to follow.

Ask before you answer
  • Is the list coming from an Apex wire, or from an LDS adapter like getListUi?
  • Do the two components have any relationship in the DOM, or are they in separate page regions?
Do not say this

Call location.reload() after the save so the list is guaranteed to be current.

It works and it throws away everything else on the page — other components' state, scroll position, anything half typed. It also trains the next developer that a reload is the way to refresh, which is how a page ends up reloading three times per interaction.

LWC mid

How do two components talk to each other?

Testing How far your answer scales — parent and child is easy, two components in different places on a page is the real question.

mid

Parent to child is a public property or a public method, both with @api. Child to parent is a custom event, dispatched with new CustomEvent('rowselect', { detail: id }) and handled in the parent's markup. Those two cover most of a page. When the components are not related — two separate items on a Lightning page — neither works, and Lightning Message Service is the answer: a message channel both subscribe to, which also reaches Aura and Visualforce on the same page.

senior

I try hard to keep it to properties down and events up, because that keeps one component owning each piece of state and makes both halves testable. The interesting decisions are at the edges. Events do not cross the boundary of a Lightning page region, so two unrelated components need a message channel, and a channel is a public contract — once two teams subscribe to it, the shape of message.detail is an interface you cannot quietly change. I would also rather pass an Id and let the child fetch than pass a large object down, because Lightning Data Service will already have that record cached and passing it means two components holding copies that drift. What I avoid now is the old pubsub module: it worked, it never crossed the page region boundary properly, and Lightning Message Service does the same job with a declared channel that shows up in metadata rather than in a shared JavaScript file nobody owns.

Deep dive on this
LWC mid

A component has to show 5,000 rows. How do you build it?

Testing Which constraint you reach for first — the browser's, or the platform's.

mid

I would not send 5,000 rows to the browser. The Apex method pages, the component asks for a page at a time, and lightning-datatable with enable-infinite-loading loads more as the user scrolls. Two things bite if you skip that: the DOM cost of rendering thousands of rows makes the page feel broken on a laptop, and an Apex method returning that much data is a serialisation cost on every call. If the user's real task is finding one row, a filter is a better answer than any amount of pagination.

senior

The first question is what they are doing with 5,000 rows, because the honest answer is usually that they are looking for a handful and a report would serve them better than a component. Assuming it is genuinely a working list, the design is server-side paging with a keyset rather than OFFSETOFFSET is capped at 2000 in SOQL and gets slower as it grows, where filtering on the last Id seen stays flat. On the client side the two costs are DOM size and re-render work: getters recompute on every render, so a getter that maps 5,000 rows into display objects runs on every keystroke elsewhere in the component, and that is usually the actual slowness rather than the row count. I would precompute the display shape once when the page arrives and keep the template dumb. And I would measure before optimising, because the browser profiler will tell me whether the time is in the callout, the getter, or the render, and those have three different fixes.

Senior round

Senior LWC questions

LWC senior

How do you handle errors in a component so a user knows what to do?

Testing How much you distinguish between logging an error and telling somebody something useful.

mid

Every path needs a handler, and there are three. A wire puts its failure in error rather than throwing, so the template needs a branch for it. An imperative call rejects, so it needs a catch. And a DML error from Apex is not one message — it arrives as error.body.pageErrors and error.body.fieldErrors, so pulling out error.body.message alone will show undefined for exactly the errors users care about, the validation rules. I normalise all of them through one helper and show the result in a toast, or inline if the error belongs to a field.

senior

The part people skip is that a platform error message and a useful message are rarely the same string. A validation rule's text is written for someone who knows the object; System.LimitException means nothing to a salesperson; and either way the user needs to know whether their work was saved. So I split it: log the raw error with enough context to find it later, and show the user a message about their situation — what failed, whether to retry, and whether anything was written. Where the error is a field-level one I put it on the field rather than in a toast, because a toast disappears and the form is still wrong. errorCallback is worth having on a container component as a backstop, with the caveat that it only catches descendants' lifecycle and render errors, so it is a safety net for the crash case and not a substitute for handling the promise. And an error that nobody can act on should not be shown at all — it should be logged and the component should degrade to something usable.

LWC senior scenario

The Jest tests all pass and users still hit the bug. What are those tests not covering?

Testing What you believe a passing unit test proves. This separates coverage from confidence quickly.

What they tell you

A component has 90% Jest coverage. Users report that saving sometimes shows a success toast while the record is unchanged. Every test passes, including one that asserts the toast fires.

mid

A success toast with no saved record almost always means the toast is not waiting for the save. In Jest the Apex import is a mock that resolves immediately, so a missing await is invisible — the promise happens to be settled by the time the assertion runs. Against a real org the call takes 200 milliseconds and the toast fires first. So I would look for the missing await, then add a test where the mock rejects, which is the case no existing test covers.

senior

Jest for a Lightning component tests the component's own logic and almost nothing about the platform, and being precise about that boundary is the answer here. The Apex modules are mocked, so nothing about permissions, sharing, validation rules or field access is exercised — a method that throws INSUFFICIENT_ACCESS for half the users passes every test. The wire adapters are emitted by hand, so the real timing never happens, which is exactly how a missing await survives. And base components are stubs, so a lightning-record-edit-form that rejects a required field is never rejecting anything. What I would change is the mix rather than the number: Jest for logic and edge cases including rejection paths, Apex tests for the server behaviour, and a small number of real end-to-end checks against a scratch org for the things only the platform can tell you. Coverage of 90% across mocked boundaries is a real number measuring the wrong thing, and saying that plainly is usually more valuable than adding tests.

Ask before you answer
  • Is the Apex module mocked in the test, and does the mock ever reject?
  • Does the component await the save promise before dispatching the toast, or dispatch alongside it?
Do not say this

Raise the coverage target and add tests for the remaining branches.

The bug is in a branch that already has a test — the toast test passes because the mock always resolves. More tests of the same shape will also pass. The gap is what the mocks assert, not how many lines they reach.

Deep dive on this
LWC senior

What does Lightning Web Security change for you as a developer?

Testing Whether you have shipped a component that depended on a third-party library, or only heard the names.

mid

It replaces Locker Service for Lightning web components. Locker gave each namespace a heavily restricted view of the DOM and of standard JavaScript, which is why plenty of third-party libraries simply did not run. Lightning Web Security isolates namespaces from each other rather than from the language, so more ordinary JavaScript works, and a library that Locker refused because it reached for document or window directly often loads without change. Aura components stay under Locker, so an org with both has two security models running side by side.

senior

The practical difference is that the failure mode moved. Under Locker, a library either worked or threw immediately, so you knew early. Under Lightning Web Security the language is largely intact and what you get instead is distortion — certain browser objects are handed to your code as a wrapped version, so a library that reaches for the real window or walks outside its own DOM can behave subtly differently rather than failing. That makes testing in the actual context non-negotiable: a library that passes in Jest is telling you nothing about this, because Jest is not running the security architecture at all. The other thing I keep in mind is that isolation is per namespace, so two managed packages cannot reach into each other, and code that used to share a global between components is a design that no longer holds. And it is enabled per org, so an inherited org might still be on Locker for everything, which changes what advice is even applicable.

Deep dive on this
LWC senior

Six components on a page need to agree about the same state. How do you build that?

Testing Where you put the single source of truth, and whether you know there is no framework answer to lean on.

mid

There is no store built into the framework, so the first choice is whether the six components can live under one parent. If they can, the parent holds the state, passes it down as @api properties and listens for events — no extra machinery, and it is easy to test. If they cannot, because they are dropped into separate regions of a Lightning page by an administrator, then a message channel is the mechanism and the components each keep their own copy in step. What I avoid is six components each fetching the same record.

senior

I would push hard on collapsing them into one parent first, because a single owner is the only design where "they disagree" is impossible rather than unlikely. When the page layout genuinely prevents that, Lightning Message Service is the transport, and then the real work is deciding what the messages mean: a payload of accountId ages well, a payload of accountRecord does not, because six subscribers then hold copies that go stale independently. Passing Ids and letting each component read through Lightning Data Service gives me one cache doing the de-duplication, which is closer to a store than anything I would write. The trap I have walked into is a shared module holding state in a module-level variable — it looks like a tidy singleton and it survives across component instances in ways that produce a bug you cannot reproduce, because the state depends on what the user visited earlier. If state has to outlive a component, it belongs on the server or in the URL, both of which are inspectable.

Deep dive on this
LWC senior scenario

A component works on a record page and fails in an Experience Cloud site. What do you check?

Testing That a community is a different runtime, not the same page with different styling.

What they tell you

A component calls an @AuraEnabled method and renders a chart using a third-party library from a static resource. Internally it works. On the public site the chart area is empty and the data never arrives.

mid

Two separate failures with two separate causes, and I would not assume either is the other. The data is a permission question: a guest user has no access to an Apex class unless the guest profile grants it, so the wire fails with an access error that nothing on the page is rendering. The chart is a content security policy question: a public site restricts where scripts can come from, so a library from a CDN is blocked while the same library in a static resource, loaded through lightning/platformResourceLoader, is served from the site's own domain. I would confirm both by opening the browser console as a genuinely logged-out visitor rather than in a preview.

senior

The first thing I would change is how it is being tested, because a preview inside Experience Builder runs as me and hides both of these. Testing as an actual guest in a private window is what makes the two failures visible and distinct. Then they separate cleanly. The data path needs the Apex class granted to the guest profile, and it needs a hard look at what that method returns, because anything a guest user can call is effectively public — I would rather that method return the three fields the chart needs than a list of records. The script path needs the library in a static resource and loaded with loadScript from lightning/platformResourceLoader, which also solves the CSP question without touching site policy. What I would write down afterwards is that guest access is a deliberate design surface: every @AuraEnabled method reachable by a guest is an unauthenticated API, and treating it that way from the start is much cheaper than auditing it after launch.

Ask before you answer
  • Is the failing user a guest user, and has the Apex class been granted to that guest profile?
  • Does the site's CSP configuration allow the script, and is the library loaded from a static resource or a CDN?
Do not say this

Turn off the site's content security policy restrictions so the script loads.

That trades a broken chart for a public site with its script protections relaxed, which is the wrong direction on the one page in the org that strangers can reach. A static resource is served from the site's own domain and needs no relaxation at all.