Alternar tema

Como personalizar páginas de erro 404 e 500 no Next.js

Easton editorial illustration: monorepo project desk

Sexta-feira, às três da tarde, o gerente de produto mandou de repente uma captura de tela no grupo: “Este é mesmo o nosso site? Está muito feio.”

Abri a imagem e vi uma tela branca, texto preto e a mensagem seca “404 This page could not be found”. Foi constrangedor.

Depois vieram os dados: 40% dos usuários fechavam a aba imediatamente após encontrar a página 404 padrão.

Ao desenvolver um projeto em Next.js, quase sempre concentramos a atenção nas páginas que funcionam normalmente: a página inicial precisa ser bonita, as listas precisam ser fluidas e as páginas de detalhes precisam estar impecáveis. E as páginas de erro? Parece que ninguém se importa, afinal o usuário não as vê com frequência.

Esse número me fez acordar. A página de erro não é um detalhe decorativo; é sua última chance de manter o usuário no site. Imagine que alguém clique em um link quebrado querendo explorar seu conteúdo e encontre uma tela branca sem design, apenas com uma mensagem fria de “página não encontrada”. Sem navegação, busca ou qualquer orientação. A reação natural é pensar: “Este site é confiável?”

Felizmente, o App Router do Next.js oferece um mecanismo completo de tratamento de erros. not-found.tsx cuida dos erros 404, error.tsx trata erros em tempo de execução e global-error.tsx protege o aplicativo inteiro como último recurso. Parece simples? Há várias armadilhas.

Na primeira vez em que configurei tudo, o código de status HTTP insistia em retornar 200 em vez de 404, e o Google acabava tratando minhas páginas de erro de forma incorreta. Em outra ocasião, os estilos de global-error.tsx simplesmente não funcionavam. Só depois de procurar bastante na documentação descobri que esse arquivo não aceita importação de módulos CSS.

A seguir, vou mostrar como configurar as páginas de erro do Next.js: desde o uso básico de not-found.tsx, passando pelo limite de erro de error.tsx, até o design de uma página 404 que realmente ajude a manter o usuário. Os exemplos estão completos e incluem as armadilhas que encontrei na prática.

Como funciona o tratamento de erros no Next.js

Quando comecei a usar o App Router, demorei a entender a diferença entre esses três arquivos. not-found.tsx, error.tsx e global-error.tsx têm nomes parecidos, mas cumprem funções completamente diferentes.

A função de cada arquivo de erro

Em resumo:

  • not-found.tsx — trata especificamente erros 404 e aparece quando a página não existe
  • error.tsx — trata erros em tempo de execução, como falhas no carregamento de dados ou exceções no código
  • global-error.tsx — é o último recurso, acionado até quando o layout raiz falha

Talvez você se pergunte por que são necessários três arquivos. Um único error.tsx não seria suficiente?

O motivo é que o tratamento de erros do Next.js funciona por níveis, como uma boneca russa. error.tsx só captura erros na mesma rota e nas rotas filhas; ele não captura um erro no layout.tsx do próprio nível. E se o layout raiz falhar? É nesse caso que global-error.tsx entra em ação.

Já not-found.tsx tem uma posição especial: sua prioridade é maior que a de error.tsx. Quando você chama a função notFound(), o Next.js ignora error.tsx e renderiza diretamente not-found.tsx.

A localização dos arquivos é essencial

Os três arquivos podem existir em diferentes níveis da árvore de rotas. A localização define o alcance de cada um.

Arquivos de erro no nível raiz (dentro do diretório app/):

app/
├── layout.tsx
├── not-found.tsx        ← página 404 global
├── error.tsx            ← tratamento global de erros
├── global-error.tsx     ← último recurso para o layout raiz
└── page.tsx

Arquivos de erro no nível da rota (dentro de uma rota específica):

app/
├── blog/
│   ├── [slug]/
│   │   ├── page.tsx
│   │   ├── not-found.tsx    ← 404 exclusivo dos artigos do blog
│   │   └── error.tsx         ← página de erro exclusiva do blog

Se o usuário acessar /blog/artigo-inexistente, o Next.js exibirá primeiro app/blog/[slug]/not-found.tsx, e não app/not-found.tsx na raiz. Assim, cada módulo pode ter uma página de erro com aparência própria.

A função notFound(): acionar um 404 pelo código

Ter o arquivo not-found.tsx não basta; também é preciso saber quando acioná-lo.

O caso mais comum é buscar dados por um ID e descobrir que eles não existem.

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'

async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`)
  if (!res.ok) return null
  return res.json()
}

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)

  if (!post) {
    notFound()  // aciona not-found.tsx
  }

  return <article>{post.title}</article>
}

Atenção a uma armadilha: você precisa chamar notFound() antes de retornar qualquer JSX. Se parte do conteúdo já tiver sido retornada, a resposta por streaming terá começado e o código de status HTTP poderá ficar travado em 200, em vez de 404.

Na primeira vez, caí exatamente nessa armadilha e escrevi algo assim:

// Exemplo incorreto
export default async function Page({ params }) {
  const data = await fetchData(params.id)

  return (
    <div>
      {!data ? notFound() : <Content data={data} />}  // já entrou no JSX!
    </div>
  )
}

A página 404 até aparecia, mas o status HTTP era 200. O mecanismo de busca interpretava aquilo como uma página normal e o SEO ficava comprometido.

Forma correta:

export default async function Page({ params }) {
  const data = await fetchData(params.id)

  if (!data) {
    notFound()  // verifique e chame antes
  }

  return <Content data={data} />  // só retorne JSX quando houver dados
}

Primeiro valide os dados. Se houver um problema, chame notFound() imediatamente e só depois retorne o JSX. Dessa forma, o status será realmente 404.

not-found.tsx: criando uma página 404 personalizada

Com a teoria resolvida, podemos começar a implementação. Primeiro criaremos uma página 404 básica e depois adicionaremos recursos aos poucos.

Versão básica: o suficiente para funcionar

O not-found.tsx mais simples pode ser assim:

// app/not-found.tsx
import Link from 'next/link'

export default function NotFound() {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="text-center">
        <h1 className="text-6xl font-bold text-gray-900 mb-4">404</h1>
        <p className="text-xl text-gray-600 mb-8">
          Desculpe, a página que você tentou acessar não existe
        </p>
        <Link
          href="/"
          className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
        >
          Voltar à página inicial
        </Link>
      </div>
    </div>
  )
}

Salve o arquivo e acesse uma rota inexistente, por exemplo http://localhost:3000/pagina-inexistente. A página já deve aparecer.

É muito melhor que o texto preto sobre fundo branco da versão padrão. Ainda assim, é simples demais. O usuário só tem um botão para voltar à página inicial, mas talvez estivesse procurando um conteúdo específico.

Versão avançada: ofereça mais opções

Uma boa página 404 deve oferecer várias saídas. Normalmente, adiciono estes elementos:

  1. Campo de busca — permite que o usuário procure o conteúdo
  2. Links populares — encaminha o usuário para páginas conhecidas
  3. Elementos da marca — logotipo e cores mantêm a identidade visual

Veja o código completo:

// app/not-found.tsx
'use client'

import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useState } from 'react'

export default function NotFound() {
  const router = useRouter()
  const [searchQuery, setSearchQuery] = useState('')

  const handleSearch = (e: React.FormEvent) => {
    e.preventDefault()
    if (searchQuery.trim()) {
      router.push(`/search?q=${encodeURIComponent(searchQuery)}`)
    }
  }

  const popularLinks = [
    { href: '/blog', label: 'Blog de tecnologia' },
    { href: '/projects', label: 'Projetos' },
    { href: '/about', label: 'Sobre nós' },
  ]

  return (
    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100">
      <div className="max-w-2xl w-full px-6 py-12 text-center">
        {/* Número 404 em destaque */}
        <h1 className="text-9xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-indigo-600 mb-4">
          404
        </h1>

        {/* Mensagem amigável */}
        <p className="text-2xl font-medium text-gray-800 mb-2">
          Ops, esta página se perdeu
        </p>
        <p className="text-gray-600 mb-8">
          Este link pode ter expirado ou a página pode ter sido movida.<br/>
          Mas não se preocupe: você pode continuar por uma destas opções:
        </p>

        {/* Campo de busca */}
        <form onSubmit={handleSearch} className="mb-8">
          <div className="flex gap-2 max-w-md mx-auto">
            <input
              type="text"
              value={searchQuery}
              onChange={(e) => setSearchQuery(e.target.value)}
              placeholder="Busque o conteúdo que procura..."
              className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
            />
            <button
              type="submit"
              className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"
            >
              Buscar
            </button>
          </div>
        </form>

        {/* Links populares */}
        <div className="mb-8">
          <p className="text-sm text-gray-600 mb-4">Ou visite estas páginas populares:</p>
          <div className="flex flex-wrap justify-center gap-3">
            {popularLinks.map((link) => (
              <Link
                key={link.href}
                href={link.href}
                className="px-5 py-2 bg-white text-gray-700 rounded-lg border border-gray-200 hover:border-blue-500 hover:text-blue-600 transition-colors"
              >
                {link.label}
              </Link>
            ))}
          </div>
        </div>

        {/* Voltar à página inicial */}
        <Link
          href="/"
          className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 font-medium"
        >
          <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
          </svg>
          Voltar ao início
        </Link>
      </div>
    </div>
  )
}

Observe a diretiva 'use client' no início do arquivo. Por quê? O campo de busca usa useState e useRouter, que são recursos do cliente, portanto o componente precisa ser declarado como Client Component.

Esta versão é bem mais útil. Ao chegar à página 404, o usuário pode:

  • buscar diretamente o conteúdo desejado
  • acessar um dos links populares
  • voltar à página inicial se nenhuma opção resolver

Isso já pode reduzir bastante a taxa de rejeição.

Técnica avançada: rastrear erros 404

Para descobrir quais páginas inexistentes os usuários tentam acessar — algumas talvez sejam conteúdos que você deveria criar — adicione um evento de análise:

'use client'

import { useEffect } from 'react'
import { usePathname } from 'next/navigation'

export default function NotFound() {
  const pathname = usePathname()

  useEffect(() => {
    // Envie os dados à sua ferramenta de análise
    if (typeof window !== 'undefined') {
      // Exemplo com Google Analytics
      window.gtag?.('event', 'page_not_found', {
        page_path: pathname,
      })

      // Ou envie ao seu próprio servidor
      fetch('/api/analytics/404', {
        method: 'POST',
        body: JSON.stringify({ path: pathname }),
      }).catch(() => {}) // uma falha aqui não deve afetar a experiência do usuário
    }
  }, [pathname])

  return (
    // ...sua interface 404
  )
}

Depois de algum tempo, analise os dados. Você pode descobrir que:

  • muitos usuários procuram uma página antiga que foi removida → considere criar um redirecionamento 301
  • um erro de digitação em determinada URL é muito frequente → adicione uma correção automática
  • os usuários procuram repetidamente um tipo de conteúdo → talvez seja hora de criá-lo

error.tsx e global-error.tsx: tratamento de erros 500

not-found.tsx trata apenas os casos em que a página não existe. E se o código lançar uma exceção, a API sair do ar ou o banco de dados ficar indisponível? É aí que error.tsx entra em cena.

Uso básico de error.tsx

error.tsx precisa ser um Client Component, portanto a primeira linha do arquivo deve ser 'use client'.

Por que isso é obrigatório? Porque o limite de erro do React, ou Error Boundary, só pode ser executado no cliente.

// app/error.tsx
'use client'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full px-6 py-8 bg-white rounded-lg shadow-lg">
        <div className="text-center">
          <div className="text-6xl mb-4">⚠️</div>
          <h2 className="text-2xl font-bold text-gray-900 mb-2">Algo deu errado!</h2>
          <p className="text-gray-600 mb-6">
            Desculpe, houve um problema ao carregar a página
          </p>

          <button
            onClick={() => reset()}
            className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"
          >
            Tentar novamente
          </button>

          <Link
            href="/"
            className="block mt-4 text-sm text-gray-500 hover:text-gray-700"
          >
            Voltar à página inicial
          </Link>
        </div>
      </div>
    </div>
  )
}

Os dois parâmetros mais importantes são:

  • error — o objeto do erro capturado, que contém message e digest, o hash do erro
  • reset — uma função que renderiza novamente o segmento da rota para tentar recuperá-lo

Ao clicar em “Tentar novamente”, reset() executa de novo o componente que falhou. Se o erro foi causado por uma oscilação de rede, a nova tentativa pode resolver.

Como lidar com mensagens de erro em produção

Há uma questão de segurança importante. No ambiente de desenvolvimento, error.message mostra a mensagem completa, por exemplo, “Database connection failed: invalid credentials”.

Em produção, isso não deve acontecer. A mensagem pode expor informações confidenciais.

No ambiente de produção, o Next.js remove automaticamente os detalhes sensíveis. O objeto error contém apenas:

  • message — uma mensagem genérica, sem os detalhes
  • digest — o hash do erro, usado para localizar o registro correspondente

Os detalhes reais aparecem nos logs do servidor. Use digest para encontrá-los:

'use client'

export default function Error({ error }: { error: Error & { digest?: string } }) {
  return (
    <div>
      <h2>Algo deu errado</h2>
      <p>{error.message}</p>
      {error.digest && (
        <p className="text-xs text-gray-400 mt-4">
          ID do erro: {error.digest}
        </p>
      )}
    </div>
  )
}

O usuário vê “ID do erro: abc123” e envia uma captura de tela. Com esse ID, você pode pesquisar nos logs do servidor e encontrar a stack trace completa.

Registrar erros em um serviço de monitoramento

Em produção, não faz sentido esperar que o usuário avise sobre cada falha. Registre os erros de forma proativa em um serviço como Sentry ou Datadog.

'use client'

import { useEffect } from 'react'
import * as Sentry from '@sentry/nextjs'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // Envia o erro ao Sentry
    Sentry.captureException(error)
  }, [error])

  return (
    <div className="min-h-screen flex items-center justify-center">
      <div className="text-center">
        <h2>Algo deu errado!</h2>
        <button onClick={() => reset()}>Tentar novamente</button>
      </div>
    </div>
  )
}

useEffect é acionado uma vez quando o erro ocorre e envia as informações completas ao Sentry. No painel do serviço, você poderá ver:

  • a stack trace do erro
  • informações sobre o navegador do usuário
  • a rota em que o problema ocorreu
  • o horário da ocorrência

Se algo quebrar em produção, você descobre em cinco minutos, em vez de esperar por uma reclamação.

global-error.tsx: o último recurso

error.tsx é muito útil, mas tem um ponto cego: ele não consegue capturar erros no layout.tsx do próprio nível.

É para isso que existe global-error.tsx. Ele envolve o aplicativo inteiro e consegue lidar até com falhas no layout raiz.

// app/global-error.tsx
'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <div style={{ padding: '50px', textAlign: 'center' }}>
          <h2>O site encontrou um erro grave</h2>
          <p>Estamos trabalhando na correção. Tente novamente mais tarde.</p>
          <button onClick={() => reset()}>Tentar novamente</button>
        </div>
      </body>
    </html>
  )
}

Observe três pontos essenciais:

  1. É obrigatório incluir as tags <html> e <body>
    Como o layout raiz falhou, global-error.tsx o substitui por completo. Por isso, o arquivo precisa fornecer a estrutura HTML inteira.

  2. Não é possível importar módulos CSS nem estilos globais
    O Next.js ignora importações de CSS dentro de global-error.tsx. Use apenas estilos inline ou uma tag <style>.

  3. A chance de acionamento é baixa
    O layout raiz costuma ser simples e dificilmente falha. global-error.tsx funciona mais como um seguro e raramente é exibido.

Mesmo assim, recomendo criá-lo. Se a falha realmente acontecer, ainda é melhor que mostrar uma tela em branco.

Um exemplo completo de global-error.tsx

Vamos adicionar alguns estilos para deixar a página mais apresentável:

// app/global-error.tsx
'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <style>{`
          * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
          }
          body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
          }
          .container {
            text-align: center;
            color: white;
            padding: 2rem;
          }
          h2 {
            font-size: 2.5rem;
            margin-bottom: 1rem;
          }
          p {
            font-size: 1.2rem;
            margin-bottom: 2rem;
            opacity: 0.9;
          }
          button {
            padding: 12px 32px;
            font-size: 1rem;
            background: white;
            color: #667eea;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-weight: 600;
          }
          button:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
          }
        `}</style>

        <div className="container">
          <h2>😵 O sistema encontrou um erro grave</h2>
          <p>Pedimos desculpas pelo problema inesperado.<br/>Nossa equipe já foi avisada e está trabalhando na correção.</p>
          <button onClick={() => reset()}>Recarregar</button>
          <p style={{ fontSize: '0.875rem', marginTop: '2rem', opacity: 0.7 }}>
            ID do erro: {error.digest || 'unknown'}
          </p>
        </div>
      </body>
    </html>
  )
}

Não é possível usar Tailwind nem importar um arquivo CSS. Os estilos precisam ficar em uma tag <style>. É um pouco rudimentar, mas funciona.

Boas práticas de design para páginas de erro

O código está pronto, mas o trabalho ainda não terminou. A implementação técnica é só a primeira etapa; o design é o que realmente determina se o usuário permanecerá no site.

Analisei páginas 404 de empresas como Spotify, Figma e Mailchimp e encontrei alguns pontos em comum.

Elementos essenciais: mostre um caminho ao usuário

Uma página de erro adequada deve ter, no mínimo, estes elementos:

1. Uma explicação clara, sem assustar o usuário

❌ Evite isto:

Error 404: The requested resource could not be located on the server.

Poucas pessoas entendem a mensagem. O usuário apenas pensa: “O que aconteceu? O site quebrou?”

✅ Prefira algo assim:

Ops, esta página se perdeu
Este link pode ter expirado ou a página pode ter sido movida

Use linguagem comum e não assuste o usuário com jargão técnico.

2. Um link para a navegação principal ou para a página inicial

É a saída mais básica. Pelo menos o usuário sabe como voltar a um lugar seguro.

<Link href="/" className="text-blue-600">Voltar ao início</Link>

3. Campo de busca

Talvez o usuário tenha digitado a URL incorretamente ou seguido um link antigo. Ofereça um campo de busca para que ele encontre o conteúdo desejado.

A página 404 do Spotify tem um campo de busca em destaque com a frase “Search for what you’re looking for”. É simples e direto.

4. Conteúdo recomendado ou páginas populares

Já que o usuário chegou até ali, ofereça algo útil:

  • site de blog → recomende artigos recentes
  • loja virtual → recomende produtos populares
  • produto SaaS → mostre os principais recursos

A página 404 da Netflix recomenda séries populares. Muitas pessoas clicam, começam a assistir e até esquecem o que estavam procurando antes.

5. Consistência com a marca

Logotipo, cores e tipografia devem seguir o restante do site.

A página de erro também faz parte da experiência da marca. Uma tela branca sem qualquer cuidado visual pode levar o usuário a questionar se o site é confiável.

Estratégias de design para aliviar a frustração

Além das funções, o tom da página também é importante.

Use humor para aliviar o momento

A página 404 da Figma mostra uma pequena animação em que um componente de interface se move pela tela e nunca pode ser clicado. A mensagem diz: “Hmm, we can’t find that page.”

O humor é leve. Em vez de pensar “o site quebrou”, o usuário pode até sorrir.

Não exagere. Empresas de tecnologia podem usar humor, mas em sites financeiros ou médicos ele pode parecer pouco profissional.

Ofereça uma compensação, se fizer sentido para uma loja virtual

Algumas lojas colocam um cupom na página 404: “A página se perdeu, mas aqui está um cupom de 10% de desconto.”

O usuário estava frustrado, recebe um desconto e pode acabar explorando a loja ou fazendo uma compra.

Não se esqueça dos celulares

Quarenta por cento do tráfego vem de dispositivos móveis, portanto a página de erro também precisa ser responsiva.

  • os botões devem ser grandes o suficiente para o toque, com no mínimo 44 × 44 px
  • evite excesso de texto, pois a tela é pequena
  • coloque os links mais importantes no início, onde possam ser vistos rapidamente

Já encontrei uma página 404 bonita no desktop, mas com botões tão pequenos no celular que precisei tocar três vezes para acertar “Voltar à página inicial”. Toda a experiência foi prejudicada.

Casos reais: comparação entre uma página boa e uma ruim

Exemplo ruim — um site governamental:

  • fundo branco e texto preto com “Error 404 Not Found”
  • nenhum link
  • nenhum campo de busca
  • nenhum logotipo

O usuário que encontra isso provavelmente sai do site.

Exemplo bom — Airbnb:

  • título em destaque: “We can’t seem to find the page you’re looking for”
  • campo de busca: “Try searching for hotels in Paris”
  • links recomendados: Homes, Experiences e Online Experiences
  • cores e tipografia da marca Airbnb

Mesmo sem encontrar a página original, o usuário pode se interessar pelo conteúdo recomendado e continuar navegando.

O que os dados mostram

Fiz um teste A/B no meu próprio blog:

Versão A (404 padrão):

  • taxa de rejeição: 78%
  • tempo médio de permanência: 3 segundos

Versão B (404 personalizada, com busca e artigos recomendados):

  • taxa de rejeição: 42%
  • tempo médio de permanência: 35 segundos

A taxa de rejeição caiu quase pela metade. Além disso, 20% dos usuários clicaram em um artigo recomendado e continuaram lendo.

Esse é o impacto do design. A situação é a mesma — a página não existe —, mas uma implementação perde o usuário e a outra o mantém no site.

Problemas comuns e armadilhas

Depois de trabalhar em vários projetos, encontrei algumas falhas recorrentes. Abaixo estão os problemas mais frequentes e suas soluções.

Problema 1: notFound() retorna 200 em vez de 404

Sintoma:

notFound() foi chamado e a página 404 apareceu normalmente, mas as ferramentas de desenvolvedor do navegador mostram o status HTTP 200. O Google trata essas URLs como páginas normais e o SEO fica incorreto.

Causa:

A resposta por streaming já começou e o código HTTP ficou travado em 200. Depois que o JSX começa a ser retornado, é tarde demais.

Solução:

Chame notFound() antes de retornar qualquer JSX.

// ❌ Incorreto: o código já entrou no JSX
export default async function Page({ params }) {
  const data = await fetchData(params.id)
  return <div>{!data ? notFound() : <Content data={data} />}</div>
}

// ✅ Correto: primeiro valide, depois retorne
export default async function Page({ params }) {
  const data = await fetchData(params.id)

  if (!data) {
    notFound()  // chame imediatamente
  }

  return <Content data={data} />
}

Lembre-se: primeiro valide, depois chame e só então renderize.

Problema 2: os estilos de global-error.tsx não funcionam

Sintoma:

Você importou Tailwind CSS ou um módulo CSS em global-error.tsx, mas nenhum estilo aparece na página.

Causa:

O Next.js ignora qualquer importação de CSS em global-error.tsx. Essa é uma limitação conhecida.

Solução:

Use apenas estilos inline ou uma tag <style>.

// ❌ Incorreto: a importação não terá efeito
import './styles.css'  // não funciona

export default function GlobalError() {
  return <div className="bg-blue-500">Erro</div>  // Tailwind também não funciona
}

// ✅ Correto: use uma tag <style>
export default function GlobalError() {
  return (
    <html>
      <body>
        <style>{`
          .error-container {
            background: #3b82f6;
            color: white;
            padding: 2rem;
          }
        `}</style>
        <div className="error-container">Erro</div>
      </body>
    </html>
  )
}

É uma solução um pouco rudimentar, mas funciona. Em geral, coloco o CSS em uma constante de string separada para deixar o arquivo mais organizado.

Problema 3: not-found.tsx não funciona em uma rota aninhada

Sintoma:

Você criou um 404 personalizado em app/blog/[slug]/not-found.tsx, mas, ao acessar /blog/artigo-inexistente, a página exibida ainda é o 404 da raiz.

Causa:

Normalmente há dois motivos:

  1. o arquivo está na localização errada
  2. notFound() não foi chamado em page.tsx

Solução:

Confirme esta estrutura de arquivos:

app/
├── not-found.tsx          ← 404 global
└── blog/
    └── [slug]/
        ├── page.tsx       ← precisa chamar notFound() aqui
        └── not-found.tsx  ← 404 exclusivo do blog

Depois, chame a função em page.tsx:

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug)

  if (!post) {
    notFound()  // aciona o not-found.tsx no mesmo nível
  }

  return <article>{post.title}</article>
}

Se o usuário acessar uma rota completamente inexistente, como /asdfghjkl, o Next.js acionará app/not-found.tsx na raiz.

O not-found.tsx de uma rota aninhada só é acionado quando o page.tsx correspondente chama notFound().

Problema 4: error.tsx não captura alguns erros

Sintoma:

O banco de dados falhou, mas error.tsx não foi acionado. A página ficou em branco ou mostrou o erro da raiz.

Causa:

error.tsx só captura erros no mesmo nível e nas rotas filhas. Se o erro acontecer no layout.tsx do próprio nível, ele não será capturado.

Além disso, notFound() ignora error.tsx e aciona diretamente not-found.tsx.

Solução:

Se a suspeita for um problema no layout, adicione error.tsx em um nível superior ou na raiz:

app/
├── error.tsx              ← captura erros nos componentes filhos do layout raiz
├── global-error.tsx       ← captura erros no próprio layout raiz
└── dashboard/
    ├── layout.tsx         ← um erro aqui não é capturado pelo error.tsx abaixo
    └── error.tsx          ← só captura erros de page.tsx e das rotas filhas

Se for realmente necessário capturar um erro no layout raiz, use global-error.tsx.

Problema 5: a mensagem de erro não aparece em produção

Sintoma:

No ambiente de desenvolvimento, a mensagem de erro é detalhada. Em produção, error.message mostra apenas “Application error”.

Causa:

Esse é um mecanismo de segurança do Next.js para evitar a exposição de informações confidenciais.

Solução:

Use error.digest para procurar as informações completas nos logs do servidor:

'use client'

export default function Error({ error }) {
  return (
    <div>
      <p>Algo deu errado: {error.message}</p>
      <p className="text-xs text-gray-400">
        ID do erro: {error.digest}  {/* mostre este código ao usuário */}
      </p>
    </div>
  )
}

O usuário pode enviar uma captura de tela; com o digest, você pesquisa nos logs do servidor, como Vercel, Sentry ou Datadog, e encontra a stack trace completa.

Outra opção é usar useEffect dentro de error.tsx para enviar o erro ao serviço de monitoramento sem esperar pelo relato do usuário.

Conclusão

Em resumo, o tratamento de erros do Next.js tem três níveis:

  • not-found.tsx → página inexistente, erro 404
  • error.tsx → erro em tempo de execução
  • global-error.tsx → último recurso para o layout raiz

A implementação técnica não é complicada. O verdadeiro desafio está no design. Uma boa página de erro pode reduzir a taxa de rejeição de 78% para 42%; esses números vieram do meu próprio teste.

Ofereça um campo de busca, alguns links recomendados e uma mensagem humana. Isso já faz uma diferença importante.

Agora vale conferir seu projeto em Next.js: as páginas de erro ainda usam o visual padrão? Reserve meia hora para melhorá-las; seus usuários perceberão a diferença.

Se encontrar algum problema, deixe um comentário. Tentarei responder. E, se este conteúdo foi útil, compartilhe-o com alguém que também esteja trabalhando em páginas de erro no Next.js.

Criar uma página 404 personalizada no Next.js

Passo a passo para criar uma página de erro 404 personalizada no App Router do Next.js, com campo de busca e links recomendados.

  1. 1

    Step 1: Criar o arquivo not-found.tsx

    Crie o arquivo not-found.tsx dentro do diretório app para usá-lo como página 404 global.
  2. 2

    Step 2: Adicionar os componentes básicos da interface

    Importe o componente Link do Next.js e crie uma interface básica com uma mensagem de erro e um botão para voltar à página inicial.
  3. 3

    Step 3: Adicionar a diretiva 'use client'

    Se precisar de gerenciamento de estado ou recursos interativos, como um campo de busca, adicione a diretiva 'use client' no início do arquivo.
  4. 4

    Step 4: Implementar a busca

    Use useState para controlar o texto da busca e useRouter para redirecionar o usuário aos resultados.
  5. 5

    Step 5: Adicionar links populares

    Crie uma lista de links para páginas recomendadas e renderize as opções de navegação com o componente Link.
  6. 6

    Step 6: Melhorar os estilos

    Use Tailwind CSS ou outra solução de estilos para melhorar a aparência da página e manter a identidade visual da marca.
  7. 7

    Step 7: Acionar o 404 no componente da página

    No page.tsx de uma rota dinâmica, chame a função notFound() quando os dados solicitados não existirem.
  8. 8

    Step 8: Testar e validar

    Acesse uma rota inexistente e confirme nas ferramentas de desenvolvedor do navegador que o código de status HTTP é 404.

FAQ

Qual é a diferença entre not-found.tsx, error.tsx e global-error.tsx no Next.js?
not-found.tsx trata especificamente erros 404, quando a página não existe; error.tsx trata erros em tempo de execução, como falhas no carregamento de dados; global-error.tsx é o último recurso e consegue capturar até erros no layout raiz. Juntos, eles formam três camadas de proteção.
Por que o status HTTP continua sendo 200, e não 404, depois de chamar notFound()?
Isso acontece quando notFound() é chamado depois que o JSX começou a ser retornado. Nesse momento, a resposta por streaming já foi iniciada e o status fica travado em 200. Valide os dados e chame notFound() antes de retornar qualquer JSX.
Por que global-error.tsx não pode usar Tailwind CSS nem importar arquivos CSS?
O Next.js ignora importações de CSS em global-error.tsx porque esse arquivo precisa substituir completamente o layout raiz. Para adicionar estilos, use estilos inline ou uma tag <style>. Essa é uma limitação do framework.
Como rastrear quais páginas inexistentes os usuários tentaram acessar?
Use useEffect e o hook usePathname em not-found.tsx para enviar o caminho que gerou o 404 ao Google Analytics ou ao seu próprio servidor. Esses dados ajudam a identificar conteúdo que precisa ser criado e URLs antigas que devem receber redirecionamento 301.
O que uma página 404 personalizada deve ter para reduzir a taxa de rejeição?
Uma boa página 404 deve incluir: 1) uma explicação clara e amigável, sem jargão técnico; 2) um link para a página inicial ou para a navegação principal; 3) um campo de busca; 4) conteúdo recomendado ou páginas populares; e 5) elementos visuais coerentes com a marca. Esses recursos podem reduzir a taxa de rejeição de 78% para 42%.

21 min de leitura · Publicado em: 5 jan 2026 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog