How to Configure a Custom 404 Error Page in ASP.NET (ASPX) for IIS Server
Highlights
- Understand the difference between ASP.NET’s customErrors and IIS’s httpErrors — and why you often need both
- Step-by-step web.config setup for a custom 404 page that works for .aspx requests
- How to make your custom 404 page also cover static files and non-ASP.NET requests
- Common reasons a custom 404 page fails to display (and how to fix each one)
- Guidance on returning the correct 404 HTTP status code instead of a “soft 404”
- Practical tips for testing your setup locally vs. on a live server
Why a Default 404 Page Isn’t Good Enough
When a visitor requests a page that doesn’t exist on an ASP.NET site running on IIS, the server’s default response is a generic, unbranded error screen. It works, technically, but it does nothing for user experience and gives visitors no way to continue browsing your site. It can also look unprofessional to anyone landing on it from a broken link or old bookmark.
Configuring a custom 404 page solves this by showing a branded, helpful page — while still returning the correct HTTP 404 status code so search engines understand the page genuinely doesn’t exist. Getting this right in classic ASP.NET (Web Forms or MVC on .NET Framework) means understanding two separate configuration layers that often get confused with each other.
The Two Layers You Need to Understand
- <customErrors> — handled by ASP.NET itself
This setting lives inside <system.web> in your web.config and only applies to requests that ASP.NET actually processes — meaning .aspx, .asmx, .ascx files and similar. If you refer to a non-existing .htm page, IIS issues the standard 404 error, but if you refer to a non-existing .aspx page, ASP.NET issues the custom 404 error instead, provided it’s configured.
- <httpErrors> — handled by IIS itself
This setting lives inside <system.webServer> and works at the web server level, catching all requests — static files, missing directories, and anything ASP.NET never gets a chance to touch. This handles all requests, whether they’re in fact handled by ASP.NET or IIS natively.
If you only configure customErrors, visitors hitting a missing .html file or a mistyped folder URL will still see IIS’s default error page. That’s why most reliable setups configure both.
Step 1: Configure customErrors in web.config
Open your project’s web.config file and add the following inside <system.web>:
<configuration>
<system.web>
<customErrors mode=”RemoteOnly” defaultRedirect=”~/Error.aspx”>
<error statusCode=”404″ redirect=”~/NotFound.aspx” />
</customErrors>
</system.web>
</configuration>
A few things worth understanding here:
- mode=”RemoteOnly” shows the detailed ASP.NET error/stack trace to you (on localhost) while showing the friendly custom page to everyone else. This is useful during development.
- mode=”On” always shows the custom error page, even locally — good for final testing before deployment.
- mode=”Off” disables custom errors entirely and shows full technical error details to everyone. Never use this on a live, public-facing site.
- You can add multiple <error> elements for different status codes (404, 500, etc.), each pointing to its own dedicated page.
Step 2: Configure httpErrors in web.config
To make sure static files and non-ASP.NET requests are also covered, add this inside <system.webServer>:
<configuration>
<system.webServer>
<httpErrors errorMode=”Custom” existingResponse=”Replace”>
<remove statusCode=”404″ subStatusCode=”-1″ />
<error statusCode=”404″ path=”/NotFound.aspx” responseMode=”ExecuteURL” />
</httpErrors>
</system.webServer>
</configuration>
A couple of details that trip people up:
- You must remove the existing 404 entry before adding your own, since IIS only allows one entry per status code. Setting subStatusCode=”-1″ clears all 404 sub-status variants (404.0, 404.1, etc.) at once.
- responseMode determines how the error page is delivered:
- ExecuteURL runs the target page through the full ASP.NET pipeline (recommended if NotFound.aspx needs to run server-side code)
- File simply serves a static file, useful if your 404 page is plain HTML
- Redirect sends the browser a redirect to a new URL — simplest, but changes the URL and can complicate status code handling
Step 3: Make Sure IIS Has the HTTP Errors Feature Installed
On self-managed or on-premises servers, the HTTP Errors role feature must be installed under IIS for httpErrors to function. Without it, you may see a blank or default response instead of your custom page. This is usually already installed on shared hosting or cloud-hosted IIS instances, but it’s worth confirming if your custom page isn’t showing up.
Step 4: Ensure the Correct Status Code Is Actually Returned
A common mistake is building a nice-looking 404 page that, technically, returns a 200 OK status code — making it a soft 404. Search engines interpret this as “this page exists and is valid content,” which can hurt how your site is crawled and indexed over time.
If you’re using an ASPX page (not a static HTML file) as your custom 404 page, explicitly set the status code in the page’s code-behind:
protected void Page_Load(object sender, EventArgs e)
{
Response.StatusCode = 404;
Response.TrySkipIisCustomErrors = true;
}
The second line is important: by default, IIS may override your response body with its own generic error page whenever it sees a 400–500 status code already set. TrySkipIisCustomErrors = true tells IIS not to do that, since your ASPX page has already written its own content.
Step 5: Test in Both Local and Live Environments
Before deploying:
- Set customErrors mode=”On” temporarily and browse to a non-existent .aspx URL locally to confirm your custom page appears
- Test a non-existent static file (like a missing .jpg or .html) to confirm httpErrors is catching it too
- Use a browser network inspector or a tool like curl -I to confirm the response status code is genuinely 404, not 200
- Switch back to mode=”RemoteOnly” before pushing to production, so local debugging still shows full error details when needed
Handling ASP.NET MVC Applications
If you’re working with an ASP.NET MVC application rather than Web Forms, the same httpErrors configuration in web.config still applies for requests that never reach your MVC routes (like a completely unmatched URL pattern). However, if a controller action deliberately returns HttpNotFound(), that’s a status code set directly by your code — IIS’s httpErrors block will still catch and replace it the same way, as long as existingResponse=”Replace” is set.
Key Takeaways
- customErrors in <system.web> only covers ASP.NET-handled file types like .aspx; it won’t catch static file 404s
- httpErrors in <system.webServer> covers everything, including static files and unmatched routes, and is necessary for full coverage
- Always remove the default status code entry before adding a custom one in httpErrors, using subStatusCode=”-1″ to clear all variants
- Explicitly set Response.StatusCode = 404 and Response.TrySkipIisCustomErrors = true in your ASPX code-behind to avoid a soft 404
- Use mode=”RemoteOnly” during development and switch only after confirming your custom page works correctly
- The HTTP Errors feature must be installed in IIS for httpErrors settings to take effect
- Test with a status-code checking tool, not just a visual check, to confirm you’re returning a genuine 404
FAQ
-
What’s the difference between customErrors and httpErrors in ASP.NET?
customErrors is an ASP.NET-level setting that only applies to requests processed by ASP.NET, such as .aspx pages. httpErrors is an IIS-level setting that applies to all requests on the server, including static files and unmatched URLs, making it necessary for complete 404 coverage.
-
Why does my custom 404 page show up locally but not on my live server?
This is often due to the customErrors mode setting. RemoteOnly shows the ASP.NET detailed error page to local requests and the custom page only to remote visitors, which is expected behavior — not a bug.
-
Why is my custom 404 page returning a 200 status code instead of 404?
This usually happens because the status code was never explicitly set on the page. Set Response.StatusCode = 404 in the code-behind, along with Response.TrySkipIisCustomErrors = true, so IIS doesn’t override your custom content.
-
Do I need to install anything extra on IIS for httpErrors to work?
Yes, the HTTP Errors feature must be enabled under IIS’s role services. This is typically pre-installed on most hosting environments, but it’s worth checking if custom pages aren’t displaying as expected.
-
Can I use a static HTML file instead of an ASPX page for my 404 error?
Yes. Use responseMode=”File” in your httpErrors configuration and point the path to your HTML file. Just be aware that a static file can’t set its own HTTP status code, so IIS’s httpErrors block handles that part for you automatically.
-
Will a custom 404 page help my SEO?
A properly configured 404 page — one returning a real 404 status code with a helpful, navigable design — supports better crawling behavior and a smoother user experience. It’s one part of good technical SEO practice rather than a guaranteed ranking factor.
-
Does this configuration work the same way in ASP.NET Core?
No. ASP.NET Core uses a different approach through middleware, typically UseStatusCodePages() or UseExceptionHandler() in Program.cs, rather than the web.config based customErrors/httpErrors system covered here, which applies to ASP.NET Framework (Web Forms and MVC) running on IIS.
Conclusion
Configuring a custom 404 error page in ASP.NET on IIS comes down to correctly layering two settings: customErrors for ASP.NET-handled requests and httpErrors for everything else at the server level. Skipping either one usually results in a custom page that only works some of the time. Just as important is making sure your page actually returns a real 404 status code rather than silently becoming a soft 404 — a small code-behind adjustment that protects both user experience and how search engines interpret your site’s structure.

