skip to content

In Expo Router, what renders when a URL or deep link matches no file under src/app, and how does +not-found.tsx customise it?

level: juniorimportance: should knowfreq 30%

answer

  1. native apps still get bad URLs
  2. a generated Unmatched screen
  3. +not-found.tsx replaces it
  4. matched last, after catch-alls
  5. nested 404s per folder

basics

~10 s

Expo Router always adds a not-found route that renders its built-in Unmatched screen; a src/app/+not-found.tsx default export replaces it, and a +not-found.tsx inside a folder handles unmatched URLs under that folder.

solid answer

~40 s

A native app has no server, but it still receives URLs that match nothing: an old deep link, a typo in a notification payload, a renamed route. Expo Router adds a **not-found route** to every app, rendering its built-in `Unmatched` screen ("Unmatched Route", with a way back). To customise it, create `src/app/+not-found.tsx` with a default export, ideally with a link to `/`. The not-found route is **matched last**, after static, dynamic and catch-all routes, so a `[...rest]` route in the same folder wins over it. A `+not-found.tsx` inside a folder, such as `src/app/cookbook/+not-found.tsx`, handles unmatched URLs under that folder, and the unmatched segments arrive in a `not-found` search parameter. On web the same route is served last, with a 404 status code.

code

tsx · 21 lines
tsx
// src/app/+not-found.tsx
import { Link, Stack } from 'expo-router';
import { StyleSheet, Text, View } from 'react-native';

export default function NotFoundScreen() {
  return (
    <View style={styles.container}>
      <Stack.Screen options={{ title: 'Recipe not found' }} />
      <Text style={styles.title}>That recipe page does not exist.</Text>
      <Link href="/" style={styles.link}>
        Back to all recipes
      </Link>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 },
  title: { fontSize: 18, fontWeight: '600' },
  link: { marginTop: 16, fontSize: 16 },
});

go deeper

for a junior

Recall that Expo Router shows a built-in Unmatched screen for unknown URLs and that src/app/+not-found.tsx with a default export replaces it.

for a middle

Explain that the not-found route sorts last, so catch-all routes win, and that nested +not-found files handle unknown URLs under their folder.

for a senior

Plan for stale deep links from old releases and notifications: context-aware nested 404s, reading the not-found segments, and a recovery link.

for a principal

Decide how URL changes are managed across releases so renamed routes do not strand users, weighing nested fallbacks against redirects for retired paths.

## Why a native app needs a 404 screen On the web a 404 is a server response. A native app has **no server**, yet it still receives URLs that match nothing: - a push notification built with a route that was later renamed; - a deep link from an older release, an email or a QR code; - a typo in a hand-written `href` inside the app. Because every Expo Router screen has a URL, every one of these ends in route matching, and something must render when nothing matches. ## The generated fallback Expo Router **always** adds a route named `+not-found` to the root of the tree when you do not provide one. It renders the built-in **`Unmatched`** component: a screen headed "Unmatched Route" with the text "Page could not be found.", the URL that failed, and a button that goes back when there is history or replaces the screen with `/` when there is not. It also sets the screen's title to "Not Found". It is functional but generic, so most apps replace it. ## Customising it with +not-found.tsx Create **`src/app/+not-found.tsx`** with a default export. Anything can render there; the Expo guide recommends a link to `/` so the user can recover. You can also re-export the default with `export { Unmatched as default } from 'expo-router'` while you design your own. The `+` prefix is **reserved**. Among nested route files, `+not-found` is the only plus-named screen allowed (API routes use a `+api` suffix instead); any other file such as `src/app/cookbook/+missing.tsx` is rejected with an error telling you to rename it. A few other `+` files, such as `+html` and `+native-intent`, are special top-level files rather than screens. ## Matching order and nested 404s The not-found route is a **deep** match: it can absorb any number of segments, and it **sorts last**: 1. Static routes such as `settings`. 2. Dynamic routes such as a `[id]` segment. 3. Catch-all routes such as `[...rest]`. 4. The `+not-found` route. Two consequences follow: - A catch-all `[...rest].tsx` in the same folder matches before `+not-found`, so it will swallow every unknown URL under that folder. - A **nested** `src/app/cookbook/+not-found.tsx` handles unmatched URLs **under `/cookbook`**, while the root one handles everything else. Expo Router's own tests cover this "multi-level 404" case. The unmatched segments are exposed to the screen as a **`not-found` search parameter** holding the segments as an array, so `/cookbook/pies/apple` arrives with `['pies', 'apple']`. That lets a cookbook-level 404 suggest "no collection called pies" rather than a generic message. | File | Handles | Typical content | |---|---|---| | none | every unmatched URL | built-in `Unmatched` screen | | `src/app/+not-found.tsx` | unmatched URLs app-wide | branded message, link to `/` | | `src/app/cookbook/+not-found.tsx` | unmatched URLs under `/cookbook` | "collection not found", link to `/cookbook` | ## Designing a useful fallback A good not-found screen in a mobile app does more than apologise: - It offers a **way forward**, usually a link to `/` or to the nearest section's index, because on a cold start from a stale link there may be no history to go back to. - It keeps the **navigator chrome** where possible: a nested `+not-found.tsx` inside the cookbook folder renders inside that section's layout, so the user still sees familiar headers. - It uses the **unmatched segments** to explain what failed, for example naming the missing collection. - It is worth **logging** the unmatched path, because a spike usually means a release renamed a route that notifications or old builds still send. ## Web and server output On web, Expo Router's route priority serves the not-found route **last**, after files in the `public` directory, regular routes and API routes, with an **HTTP 404 status code**. On Android and iOS there is no status code; the screen is simply pushed like any other route. ## What interviewers look for - Knowing that a native router needs a 404 at all, and why. - Knowing the file name and that it is a default export, not a config entry. - Knowing it sorts last, so a catch-all route hides it. - Using a nested `+not-found.tsx` to give different parts of the app context-aware recovery paths.

  • Why might src/app/cookbook/+not-found.tsx never render even for clearly bad cookbook URLs?
    A catch-all route such as `src/app/cookbook/[...rest].tsx` sorts ahead of the not-found route and matches any number of segments, so it takes every unknown URL under `/cookbook` first. Remove the catch-all or handle the missing case inside it.
  • Can a file called src/app/cookbook/+missing.tsx serve as a folder-level 404?
    No. Apart from `+not-found` and `+api` files, nested route files may not start with `+`; Expo Router rejects the file and tells you to rename it. The folder-level fallback must be named `+not-found.tsx`.

saying these in an interview costs you the question

  • Native apps cannot hit unmatched routes because there is no server.
  • Without +not-found.tsx an unmatched deep link crashes the app.
  • +not-found.tsx is matched before dynamic and catch-all routes.
  • Only one +not-found.tsx is allowed, at the root of src/app.
  • The fallback is configured in app.json rather than as a route file.