skip to content

Why can hasPreviousPage be false on a GraphQL connection page that has items before it?

level: middleimportance: should knowfreq 47%

answer

  1. One boolean is exact, one is best-effort
  2. Which direction is the client travelling?
  3. The algorithm's second branch says may
  4. False can mean I did not check
  5. The client knows it sent a cursor

basics

~20 s

The Relay server specification only requires hasPreviousPage to be accurate when a client pages backward. Paging forward, a server may answer true only if it can cheaply tell that earlier items exist, and false otherwise. False therefore means no, or unknown.

solid answer

~50 s

The convention defines both booleans with an explicit escape hatch. `hasPreviousPage` is required to be accurate only when the client asked for a backward slice: apply the cursors, and if more edges remain than were requested, it is true. If instead the client is paging forward from a cursor, the server *may* answer true when it can efficiently determine that elements exist before that cursor, and otherwise answers false. `hasNextPage` is the mirror image — required to be accurate for a forward slice, best-effort for a backward one. So on a forward page, `hasPreviousPage: false` carries almost no information: it may mean "this is the first page" or "I did not check". A client must not drive a Previous control from it; the reliable signal is its own paging state. Servers that can answer truthfully in both directions are permitted to, and many do — but a client that relies on it is relying on a behaviour the convention does not require.

code

graphql · 16 lines
graphql
query GalleryPage($id: ID!, $cursor: String!) {
  gallery(id: $id) {
    artworks(first: 24, after: $cursor) {
      edges {
        cursor
        node { accessionNumber title }
      }
      pageInfo {
        hasNextPage
        hasPreviousPage
        startCursor
        endCursor
      }
    }
  }
}

go deeper

for a junior

Know that the two booleans are not equally trustworthy, and that a false hasPreviousPage does not by itself prove you are on the first page of a list.

for a middle

Explain the algorithm's two branches — exact when the matching count argument is set, best-effort when only a cursor is — and say which boolean that makes reliable for a forward-paged request.

for a senior

Demonstrate the consequence on both sides: a Previous control driven off this field strands users mid-list, and on the server you decide whether one extra bounded read per page is worth paying to answer honestly.

for a principal

Frame it as portability risk. A client tuned to a generous server breaks against a compliant one that answers false, so decide whether your platform guarantees truthful booleans in both directions and write that guarantee into the schema guidelines rather than leaving it to each team.

## The rule, stated precisely The Relay server specification does not define the two page-info booleans symmetrically with respect to effort. Each has two branches, and only the first branch is an obligation. **`hasPreviousPage`** - If the client requested a **backward** slice: take the edges that remain after applying the client's cursors, and return true if there are more of them than the client asked for, false otherwise. This branch is exact. - Otherwise, if the client supplied an **after** cursor: the server may return true **if it can efficiently determine** that elements exist before that cursor. - Otherwise, false. **`hasNextPage`** is the mirror: exact when the client requested a forward slice, best-effort when the client supplied only a **before** cursor, false otherwise. The phrase that does all the work is "if the server can efficiently determine". It is permission, not obligation. A server that shrugs and returns false is compliant. ## Why the escape hatch exists The natural implementation of forward paging is a keyset read: everything ordered after the position the cursor names, bounded by the page size. That read knows a great deal about what comes *after* the cursor and nothing whatsoever about what comes before it. Answering `hasPreviousPage` honestly means a second read in the opposite direction — usually one row on the same index, and usually cheap. But not always: the source may be a remote service that only pages one way, an append-only log with no backward index, or a filter that is expensive to evaluate in either direction. The convention refuses to mandate work that some servers cannot do cheaply, so it makes the answer optional and pins the fallback to false. ## What that does to a client A museum collection graph pages a gallery's objects 24 at a time. The client is three pages in and asks for the next 24 objects after the cursor it holds; 48 objects precede this page. A fully compliant server may answer: ```json { "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, "startCursor": "b3B1czoxNzcy", "endCursor": "b3B1czoxNzk1" } } ``` `hasPreviousPage: false` here is a shrug, not a fact. A user interface that hides its Previous control whenever that field is false will strand the reader on page three with no way back. The correct client rule is simple: **you know you have a previous page because you supplied a cursor to get here**, and because you kept the cursors of the pages you came from. On a forward-paged list, `hasPreviousPage` is at best a hint. ## The property worth memorising *The boolean in the direction you are travelling is the reliable one.* Page forward and `hasNextPage` is exact while `hasPreviousPage` is best-effort. Page backward and the roles swap: `hasPreviousPage` becomes exact and `hasNextPage` becomes the shrug. This is why `hasNextPage` feels trustworthy in practice and `hasPreviousPage` does not — it is not a difference between the fields, it is a difference in the direction almost every client happens to travel. ## The case where false is simply correct On the very first page — a forward request with no cursor at all — neither branch of the algorithm can produce true, so `hasPreviousPage` is false and that false is exactly right. Do not read laziness into it. The ambiguity only appears once a cursor is in play. ## If you want it to be truthful A server that wants to answer both booleans honestly on every page issues one extra bounded read in the opposite direction: does at least one element exist before this cursor, under the same filter and the same total order? On an indexed unique ordering this is a single-row lookup and costs almost nothing next to the page itself. Measure it before committing, because it is a per-page cost on every connection field in the schema, and if it is not cheap the convention's own answer is to return false rather than to pay. ## The interoperability trap Because truthful answers are permitted and false answers are compliant, two servers implementing the same convention can behave differently on identical requests. A client written against a generous server — one that always does the lookback — will quietly lose its Previous control when pointed at a compliant server that does not. If a platform team promises truthful booleans in both directions, that promise belongs in a written schema-design guideline, not in a client's assumptions. ## What an interviewer listens for Naming which branch of the algorithm a given request takes; saying "convention" rather than "the specification requires"; and finishing with what a client should do instead, rather than only diagnosing the server.

  • When a client pages backward, which of the two booleans becomes the reliable one?
    `hasPreviousPage`. Its exact branch is the one that fires for a backward slice: the server applies the cursors, compares what remains against the number requested, and answers precisely. `hasNextPage` then becomes the best-effort field, true only if the server can cheaply see that elements follow the cursor the client sent. The roles swap with the direction of travel.
  • How should a client decide whether to offer a Previous control on a forward-paged list?
    From its own paging state, not from the response. If it supplied a cursor to reach this page, a previous page exists by construction, and if it kept the cursors of the pages it came from it can go back to any of them. Reading `hasPreviousPage` on a forward page means trusting a field the server was never required to compute.
  • Is a server that always answers both booleans truthfully doing something wrong?
    No — it is exceeding the requirement, which is explicitly allowed. The cost is one extra bounded read per page in the opposite direction, which on an indexed unique ordering is a single-row lookup. The risk is not correctness but expectation: clients built against that server may assume behaviour that another compliant server will not provide.

It is a trail sign that only reports the path ahead. Facing forward it tells you honestly how much remains; "nothing behind you" often just means the sign-maker never walked that way.

saying these in an interview costs you the question

  • Treating hasPreviousPage false as proof of being on the first page
  • Claiming the convention requires both booleans to be exact
  • Assuming non-null means the value is always accurate
  • Calling a false answer a specification violation
  • Computing hasPreviousPage by counting every earlier row
  • Hiding a Previous control purely on the response field

context