# Why JavaScript shows a loading state before a result

Canonical page: https://yishto.com/en/learn/web-interface/javascript-runtime-and-async/

Author: [Yishto](https://yishto.com/en/about/)

Explain synchronous execution, microtasks, and await through document search, then protect loading states and current results with explicit request identity and controlled completion-order tests.

Published: 2026-11-10
Updated: 2026-11-10

After a search click, the page shows loading before the result arrives. JavaScript is not doing all the work simultaneously inside one statement. Platform facilities handle external work, then schedule its completion callbacks. Responsive feedback depends on synchronous code yielding execution opportunities and on results updating the correct state.

The document-search case explains tasks, microtasks, promises, and stale responses. The offline example models queue order without a browser, network, or timer. Python scheduling is not evidence of JavaScript scheduling.

## Finish the current synchronous work

Synchronous JavaScript on one event loop normally runs to completion. Another callback cannot arbitrarily interrupt its active stack. A long computation in a click handler can delay input and painting. Declaring the function async does not automatically move that computation to another thread.

Platform facilities can wait for networking or timers and arrange callbacks later. A timer's delay is not an exact execution time: the active task, queued work, and platform policies affect when it starts. A zero-delay timer does not execute immediately within the current call.

A Web Worker can provide another execution environment for suitable computation, with messaging, data-transfer, and lifecycle responsibilities. First identify whether main-thread computation causes the problem before introducing a worker into a document-search application.

## Understand promises and await

A promise represents eventual fulfillment or rejection. Its reaction callbacks use microtask scheduling. In a typical browser event loop, microtasks run after current synchronous work and before subsequent tasks. Continuously adding microtasks can delay other work, so promises do not provide free scheduling for unlimited computation.

Await suspends the continuation of its async function while other work can proceed. The synchronous part before await has already run, and continuation still requires scheduling. Suspending one function neither stops the whole browser nor automatically provides parallel execution.

Handle external failures by layer. A connection failure, HTTP error, and invalid response field need different checks. A fulfilled fetch promise does not establish an HTTP success status. Inspect the result, validate the response contract, and then select loading, success, empty, or error state.

## End loading on every relevant path

Record the current query before entering loading. Populated results select success; no results select empty; failures select error while retaining the query. Ending loading only in the populated-success branch leaves a spinner after rejection or an empty result.

Cleanup also needs request ownership. An older request's final cleanup must not close the loading state of a newer request. Assign an identity to each operation and let only the currently valid request update the current interface.

Closures capture bindings in their scope. Which value a callback observes depends on declarations, assignments, and invocation time. Save the query and request sequence for the particular operation rather than reread a mutable global input later. Closures do not universally create independent copies of data.

## Observe a simplified queue order

The example starts with synchronous start and end entries, drains the promise microtask, then drains the timer task. These lists are synthetic queues. Real browsers also handle rendering opportunities, multiple task sources, nested scheduling, and other platform rules.

```python
tasks = ["timer"]
microtasks = ["promise"]
observed = ["start", "end"]
while microtasks:
    observed.append(microtasks.pop(0))
while tasks:
    observed.append(tasks.pop(0))
assert observed == ["start", "end", "promise", "timer"]
print(observed)
```

The output is `['start', 'end', 'promise', 'timer']`. The assertion confirms the model's chosen ordering and illustrates why a promise callback does not interrupt the synchronous segment. It measures no duration and cannot establish when painting happens or that all browser tasks share one FIFO queue.

For an actual browser experiment, record synchronous logs, a fulfilled promise callback, and a timer callback. Compare observed order with the mechanism, while investigating timer delay and painting separately. Without running that experiment, describe the Python result as a model rather than a browser measurement.

## Reject stale search results

Suppose an index query starts before a transaction query but returns later. Assigning every response directly can replace the latest search with old results. Increase a request sequence and allow state updates only when the completing sequence matches the current one. Cancellation can reduce unnecessary work, while an identity check still protects state.

Cancellation does not guarantee that the server stopped all work or that no callback was already scheduled. Navigation, filter changes, and retries must also invalidate obsolete results. Use application-controlled request identity rather than infer freshness from completion order.

Test old-slow/new-fast and old-fast/new-slow responses, plus an old failure followed by new success. The acceptance question is which query owns the final state. Request sequence and server data version are separate concepts when the service supplies versioned data.

Sending a request on every keystroke can create unnecessary load. Choose explicit submission or delayed triggering according to the product. Fewer requests do not replace stale-result checks. Delayed triggers must handle clearing input, leaving the page, and renewed typing so obsolete timers do not send irrelevant queries.

Keep request identity separate from document identity. A query ID governs who may update this interface operation; a document ID identifies a returned record. Mixing them creates mistakes on retry or repeated queries. Error messages must also belong to the current query.

## Exercise: control the search state machine

Define legal transitions among idle, loading, success, empty, and error, with query IDs and result ownership. Use manually completed request substitutes to model two searches without a real service. Complete each in different orders and inspect every state transition.

Empty results must end loading; failure must retain the query and allow retry; old success must not replace new results; old failure must not close new loading; and callbacks after departure must not update visible state. Add substantial synchronous computation in a real browser observation and inspect its effect on interaction. State explicitly that async alone does not change its execution environment, and record behavior without inventing performance multipliers.

## Sources & further reading

- [JavaScript guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide)
- [JavaScript execution model](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Execution_model)

Sources checked on 2026-11-10. Examples are teaching data, not production measurements.

## Related guides

- [Streaming and structured output solve different problems](https://yishto.com/en/learn/models/streaming-and-structured-output/)
- [Manage pages and asynchronous state in Flutter](https://yishto.com/en/learn/application-platforms/flutter-reading-library/)
