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

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:
- Campo de busca — permite que o usuário procure o conteúdo
- Links populares — encaminha o usuário para páginas conhecidas
- 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
messageedigest, 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 detalhesdigest— 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:
-
É obrigatório incluir as tags
<html>e<body>
Como o layout raiz falhou,global-error.tsxo substitui por completo. Por isso, o arquivo precisa fornecer a estrutura HTML inteira. -
Não é possível importar módulos CSS nem estilos globais
O Next.js ignora importações de CSS dentro deglobal-error.tsx. Use apenas estilos inline ou uma tag<style>. -
A chance de acionamento é baixa
O layout raiz costuma ser simples e dificilmente falha.global-error.tsxfunciona 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:
- o arquivo está na localização errada
notFound()não foi chamado empage.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
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
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
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
Step 4: Implementar a busca
Use useState para controlar o texto da busca e useRouter para redirecionar o usuário aos resultados. - 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
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
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
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?
Por que o status HTTP continua sendo 200, e não 404, depois de chamar notFound()?
Por que global-error.tsx não pode usar Tailwind CSS nem importar arquivos CSS?
Como rastrear quais páginas inexistentes os usuários tentaram acessar?
O que uma página 404 personalizada deve ter para reduzir a taxa de rejeição?
21 min de leitura · Publicado em: 5 jan 2026 · Atualizado em: 4 set 2026
Guia completo Next.js
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Gerenciamento de loading no Next.js: guia prático de loading.tsx e Suspense
Aprenda técnicas práticas de loading.tsx e Suspense no Next.js, deixe de escrever useState manualmente e crie uma experiência de carregamento profissional com menos código. Inclui skeleton screens, rotas dinâmicas e soluções para problemas comuns.
Parte 32 de 51
Próximo
Error Boundary no Next.js: 5 práticas para lidar com erros em runtime
Aprenda a usar error.tsx, global-error.tsx e reset() no Next.js, tratar erros em Server Components e criar uma recuperação segura sem deixar a página em branco.
Parte 34 de 51



Comentários
Entre com GitHub para comentar