Robust Applications

Build applications that gracefully handle schema evolution

GraphQL schemas grow over time — new types, new enum values, new union and interface subtypes — and a well-written application expects this and handles it gracefully. How much of this is handled for you depends on your environment. In languages with compile-time schema awareness — such as TypeScript with a codegen tool, Swift with Apollo iOS, or Kotlin with Apollo Kotlin — a typed client can generate catch-all variants for enums, surface nullable fields as optional types, and warn at build time when new cases are unhandled. In dynamic languages or environments without a schema-aware client, these are application-level concerns that need to be addressed explicitly in your code.

The three areas below are where applications most commonly fail to account for schema evolution.

Plan for unknown enum values

GraphQL schemas can add new enum values at any time. A Status enum that starts with ACTIVE and INACTIVE might later gain PENDING or ARCHIVED. If your application treats every enum value as exhaustively known, a new value from the server can cause crashes or silent data loss.

Avoid exhaustive switches without a fallback:

// Fragile: crashes or falls through if a new value is added
switch (status) {
  case "ACTIVE":
    return showActive()
  case "INACTIVE":
    return showInactive()
  // No default — new values are silently ignored or throw
}

Always include a default branch:

// Robust: handles any future value the server might return
switch (status) {
  case "ACTIVE":
    return showActive()
  case "INACTIVE":
    return showInactive()
  default:
    return showUnknown(status)
}

In typed languages, if your codegen tool marks enums as exhaustive, configure it to generate a catch-all variant (often called UNKNOWN or %future added value) so the compiler enforces that you handle it.

Plan for unknown union and interface subtypes

Unions and interfaces in GraphQL schemas can gain new member types over time. A SearchResult union that starts as Article | User might later gain Product. Likewise, a Node interface implemented by User and Post might later be implemented by Comment. Applications that only match known types and ignore unrecognized ones are robust; applications that crash on unexpected __typename values are not.

Query __typename on union and interface fields and handle unrecognized types:

query Search($query: String!) {
  search(query: $query) {
    __typename
    ... on Article {
      title
      url
    }
    ... on User {
      name
      avatarUrl
    }
  }
}

In your application code, handle the case where __typename is something you don’t recognize:

for (const result of data.search) {
  if (result.__typename === "Article") {
    renderArticle(result)
  } else if (result.__typename === "User") {
    renderUser(result)
  } else {
    // A new type was added — degrade gracefully instead of crashing
    renderUnknownResult(result)
  }
}

This is especially important in long-lived native mobile apps, where a user may be running an old version of the app against a schema that has since been extended.

Do not force-unwrap nullable fields

GraphQL fields are nullable by default. When a field is nullable, the schema is explicitly communicating that it may not always return a value — either because the data is genuinely optional, or because the server may omit it when an error occurs on that field without failing the entire response.

Force-unwrapping a nullable field (using ! in Swift, !! in Kotlin, or a non-null assertion in TypeScript) bypasses this contract and turns a graceful partial response into a crash.

Avoid non-null assertions on nullable fields:

// Dangerous: crashes if `user` or `profile` is null
const email = data.user!.profile!.email!

Use safe access patterns instead:

// Safe: degrades gracefully when any field is absent
const email = data.user?.profile?.email ?? "No email provided"

In Swift, prefer guard let or optional chaining over force-unwrapping:

// Dangerous
let email = data.user!.profile!.email!
 
// Safe
guard let email = data.user?.profile?.email else {
  showPlaceholder()
  return
}
showEmail(email)

If you find yourself force-unwrapping fields that you believe will always be present, consider working with the schema maintainer to mark those fields as non-null — that way the guarantee is encoded in the schema itself, and the server is responsible for upholding it.

Recap

  • Unknown enum values: Always include a default/fallback branch when switching on enum values. Configure codegen tools to generate a catch-all variant.
  • Unknown union and interface subtypes: Always query __typename on union and interface fields and handle unrecognized types gracefully instead of crashing.
  • Nullable fields: Use safe access patterns (optional chaining, guard let, null-coalescing) rather than force-unwrapping. If a field must always be present, encode that in the schema.

next lesson

Common GraphQL over HTTP Errors

Learn about common 'graphql-http' errors and how to debug them.

Go to next lesson