Skip to main content

Class 10: Case Study: GitHub and GitLab

Duration: 15 minutes | Difficulty: Intermediate | Prerequisites: Class 5 of this course.

All claims describe the two APIs as publicly documented as of September 2026, with sources linked inline; where official documentation is thin, this class says so and cites what exists.


What You'll Learn​

By the end of this class, you will:

  • Read GitHub's top-level error convention, including the type key that sits outside the spec's recommended shape
  • Read GitLab's contributor-enforced rules for where every error belongs, in their own words
  • Compare the two designs on machine-readability, discoverability, and documentation
  • Extract the portable lesson each API teaches, including one that is a cautionary tale about documentation debt

Why this pair​

Shopify showed one philosophy executed thoroughly. GitHub and GitLab are more instructive as a pair precisely because they are so comparable. Both expose large public GraphQL APIs over developer-platform domains, and both serve enormous third-party ecosystems. They placed business outcomes on opposite sides of the line this course drew in Class 5. They face the same problems, chose opposite placements, and both have shipped successfully for years, which makes the comparison unusually direct.

GitHub: everything top-level, with a nonstandard type key​

GitHub's GraphQL API keeps errors in the response's top-level errors list, including outcomes that Class 5 would call expected results of user input. Query a repository that does not exist (or that your token cannot see) and the response is partial data plus an error of this shape. The example comes from a GitHub community discussion, where a user posted the error object from a real API response:

{
"type": "NOT_FOUND",
"path": ["repository"],
"locations": [{ "line": 3, "column": 9 }],
"message": "Could not resolve to a Repository with the name 'owner/name'."
}

Three aspects of this shape matter for the rest of the class.

It is a classified error. NOT_FOUND is machine-readable, stable, and exactly the kind of code Class 4 demanded; other values like FORBIDDEN and RATE_LIMITED appear in community reports and client code. Octokit's throttling plugin, for example, checks for error.type === "RATE_LIMITED". GitHub understood the message-is-not-an-API rule.

The classification sits outside extensions. type sits directly on the error object, as a sibling of message and path. Class 1 quoted the specification's exact stance on this:

GraphQL services should not provide any additional entries to the error format since they could conflict with additional entries that may be added in future versions of this specification.

The specification pairs that sentence with a counter-example showing precisely this shape. GitHub's convention predates extensions on errors: a 2017 issue quotes a GitHub response with "type": "NOT_FOUND", and the spec added the extensions entry for errors in PR #407, merged in March 2018. GitHub has not published why it keeps the key outside extensions. It works in practice, and client libraries handle it, but it remains the pattern the specification discourages. In a design review it is useful as a concrete example of why the extensions escape hatch exists. If the spec ever standardizes a type key on errors with different semantics, GitHub has a migration problem, and extensions.type would have avoided it.

The convention is almost undocumented. GitHub's own guide to forming GraphQL calls leaves error response shapes out entirely. An issue on the GitHub docs repository, "GraphQL errors, and especially rate limit errors, are not documented", sat open for about a year before being closed as not planned in November 2023. The documentation remains unwritten. What official documentation does cover is the limits themselves, on the rate and query limits page:

  • Users get 5,000 points per hour.
  • Apps get 10,000 in several GitHub Enterprise Cloud cases, and other app installations scale from 5,000 up to 12,500.
  • A single call is capped at 500,000 total nodes.
  • A request over the primary limit still returns 200 with an error message, and a request over a secondary limit returns 200 or 403.

None of these cases comes with an example error body. Integrators learn the actual shapes from community threads and from probing the API, which for an API of this importance is the strongest argument this course can offer for treating error formats as documented, versioned API surface.

The design has a coherent logic worth stating fairly. With errors top-level and classified, GitHub gets uniform handling for free across an enormous query surface: one error channel, one shape, no per-mutation types. And path tells clients which part of a batched query failed while the rest of the data arrives normally, GraphQL's partial results doing exactly what Class 1 promised. What is lost is discoverability: nothing in GitHub's schema tells you that repository can fail with NOT_FOUND, what mutations can reject input for which reasons, or what type values exist at all.

GitLab: the line, written down and enforced​

GitLab's GraphQL API is the opposite case: the placement rules are written policy, enforced in code review, in the GraphQL API style guide that every contributor to GitLab's schema must follow. It states Class 5's line more clearly than any other public source we found, so this class quotes it at length.

The framing rule assigns every error to an audience:

Errors may be reported to users either at $root.errors (top-level error) or at $root.data.mutationName.errors (mutation errors). The location depends on what kind of error this is, and what information it holds.

Top-level errors are for the developer-facing quadrants of Class 4:

One or more non-recoverable errors can be returned at the top level. These are things over which the user has little to no control, and should mainly be system or programming problems, that a developer needs to know about.

Mutation errors are for the user: outcomes of their own input, permission situations they can understand, state conflicts they can resolve. The guide therefore requires an errors field on every mutation response, which "should be populated on failure, and may be populated on success". Introspecting gitlab.com on 22 September 2026 shows that field as errors: [String!]! on 710 of 712 mutation payloads, in the same always-present style as Shopify's userErrors. The two exceptions are a nullable [String!] on adminRolesLdapSync and the typed list described below.

Two more of the guide's sentences carry design weight beyond the placement rule. The first is about failed updates:

In an update mutation we return the current true state of thing.

That is the exact capability Class 5 showed the top-level channel cannot provide, because an error removes the mutation's result; GitLab codified the recovery-UX argument as a rule. The second is about the server-side discipline Class 6 argued for, and the guide says:

Never catch an error unless the user needs to know about it.

That restates the outcomes-versus-failures boundary. Translate what the user needs into the response type, and let everything else flow to the top-level channel as the system failure it is.

The weakness sits in the type of that errors field: [String!]! is a list of human-readable strings. No codes, no field paths, no typed variants. On Giroux's ladder from Class 5 this is the ad-hoc rung below Shopify's interface-and-enums, and it leaves GitLab's mutation errors machine-opaque. A form UI cannot reliably map "Title can't be blank" to the title input, and localization or rewording is a silent breaking change for any client that pattern-matches. One payload carries a richer type: the error list on bulkSetVulnerabilityFindingsDueDates holds entries with a machine-readable code, a findingUuid and a message. The documented convention remains strings. The comparison with Shopify shows that writing down where errors go and making them machine-readable are separate achievements; GitLab has done the first but not yet the second.

The contrast, drawn explicitly​

The table puts the two side by side against the criteria this course has developed, with Shopify from Class 9 as a reference point.

CriterionGitHubGitLabShopify (Class 9)
Where expected outcomes goThe top-level errors listAn errors field in the mutation responseA userErrors field in the mutation response
Machine-readable classificationA type key, such as NOT_FOUNDPlain stringscode enums on the newer, typed error types
What the schema reveals about outcomesNothingWhich mutations report outcomesEach mutation's error type and its codes
Where the conventions are documentedCommunity threadsGitLab's public contributor style guideShopify's API reference

GitLab follows the Class 5 line and GitHub does not, and GitHub compensates with a classification vocabulary applied uniformly. On machine-readability the ranking inverts: GitHub's type beats GitLab's plain strings, even on the layer the specification discourages. Both designs work in production. GitHub's single top-level channel gives one error shape across a very large query surface, and GitLab's mutation errors give its forms a place to show what went wrong.

The portable lessons, one per API:

  • From GitHub: classify errors first, and put the classification in extensions, which the spec reserves for implementers. Then document it. Integrators consult error documentation constantly, and without it they fall back on community threads and probing.
  • From GitLab: write the placement rule down as policy with an audience test ("what kind of error this is, and what information it holds"), and enforce it in review. Return the true state on failed updates, and give errors machine-readable structure when client code, and not only people, needs to act on them.

Summary​

GitHub and GitLab solve the same problems with opposite placements:

  • GitHub keeps everything in the top-level errors list, adding a machine-readable type key, NOT_FOUND and its relatives, directly on the error object. That is the exact placement the specification discourages in favor of extensions. The convention is also publicly undocumented: even the rate-limit error shapes are absent from official pages that otherwise specify the limits precisely, at 5,000 points per hour for users and 500,000 nodes per call.
  • GitLab writes the audience-based rule into its contributor style guide, sending unrecoverable system problems top-level and user-relevant outcomes into an always-present errors field, typed [String!]! on almost every mutation response. It returns the true state of the entity on failed updates, though its plain strings sit low on the machine-readability ladder that Shopify's typed enums top.

Each API's gap is instructive: classification needs documentation and the right layer, and written placement rules still need typed payloads before clients can act on the errors reliably.

Further Reading​

What's Next​

Both of this class's APIs are server-side designs. Class 11 turns to the organizations that shaped the client side of this course. Meta's Relay team drove semantic nullability from product pain to specification proposal. Netflix's DGS framework ships the error taxonomy that Class 4 surveyed, ready for Spring teams to reuse.