Skip to content

Commit cd4b09a

Browse files
Add error pages (#84)
Add a template and error middleware which will display nicer error messages when there’s an error in your template: <img width="965" height="611" alt="Screenshot 2025-11-22 at 23 44 18" src="https://github.com/user-attachments/assets/8f4a22e4-07a0-4631-a6af-674746de88fd" /> Borrowing heavily from [nhsuk-prototype-rig](https://github.com/x-govuk/nhsuk-prototype-rig) (thanks @paulrobertlloyd!).
1 parent 5bb6b19 commit cd4b09a

File tree

9 files changed

+119
-2
lines changed

9 files changed

+119
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ If you are upgrading a prototype which used a previous version of the kit, see [
2727
### :new: **New features**
2828

2929
- A more helpful 404 error page if you visit a path which doesn’t match a route or a template.
30+
- A more helpful error page if you have an error in your template
3031

3132
## 7.1.0 - 20 October 2025
3233

eslint.config.mjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,14 @@ export default defineConfig([
132132
},
133133
rules: {
134134
'@typescript-eslint/no-require-imports': 'off',
135-
'@typescript-eslint/unbound-method': 'off'
135+
'@typescript-eslint/unbound-method': 'off',
136+
'@typescript-eslint/no-unused-vars': [
137+
'error',
138+
{
139+
argsIgnorePattern: '^_',
140+
varsIgnorePattern: '^_'
141+
}
142+
]
136143
}
137144
},
138145
{

lib/express-middleware/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const authenticate = require('./authentication')
99
const autoRouting = require('./auto-routing')
1010
const autoStoreData = require('./auto-store-data')
1111
const productionHeaders = require('./production-headers')
12+
const renderErrorPage = require('./render-error-page')
1213
const renderPageNotFound = require('./render-page-not-found')
1314
const resetSessionData = require('./reset-session-data')
1415
const setCurrentPageInLocals = require('./set-current-page-in-locals')
@@ -116,7 +117,7 @@ function all(options) {
116117
app.use(autoRouting.matchRoutes)
117118

118119
// Page not found - this must come last
119-
app.use(renderPageNotFound)
120+
app.use(renderPageNotFound, renderErrorPage)
120121

121122
next()
122123
}
@@ -129,6 +130,7 @@ module.exports = {
129130
authenticate,
130131
productionHeaders,
131132
renderPageNotFound,
133+
renderErrorPage,
132134
resetSessionData,
133135
setCurrentPageInLocals,
134136
setSessionDataDefaults
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const renderErrorPage = function (error, req, res, _next) {
2+
error.status = error.status || 500
3+
4+
console.error(error.stack)
5+
6+
res.status(error.status)
7+
res.render('500.html', { error })
8+
}
9+
10+
module.exports = renderErrorPage

lib/views/500.html

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{% extends "layout.html" %}
2+
3+
{% set pageName = "Sorry, there is a problem with the prototype" %}
4+
5+
{% set stackHtml -%}
6+
<pre class="nhsuk-u-margin-0" tabindex="0"><code class="nhsuk-u-font-size-14">{{ error.stack }}</code></pre>
7+
{%- endset %}
8+
9+
{% block content %}
10+
<div class="nhsuk-grid-row">
11+
<div class="nhsuk-grid-column-two-thirds">
12+
13+
<h1 class="nhsuk-heading-xl">{{ pageName }}</h1>
14+
15+
<h2 class="nhsuk-heading-m">{{ error.name }}</h2>
16+
17+
<p class="nhsuk-caption-m">{{ error.message | nl2br | nl2br | safe }}</p>
18+
{{ details({
19+
summaryText: "Stack trace",
20+
html: stackHtml
21+
}) }}
22+
23+
<pre class="nhsuk-u-margin-top-8">Status code: {{ error.status }}</pre>
24+
</div>
25+
</div>
26+
{% endblock %}
27+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{% extends "layout.html" %}
2+
3+
{% block content %}
4+
<div class="nhsuk-grid-row">
5+
<div class="nhsuk-grid-column-two-thirds">
6+
{% if %}
7+
</div>
8+
</div>
9+
{% endblock %}
10+

testapp/views/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ <h1>{{ serviceName }}</h1>
5050
</form>
5151

5252
<p><a href="/custom-route">Custom route</a>.</p>
53+
<p><a href="/containing-error">Template with a syntax error</a>.</p>
5354
</div>
5455
</div>
5556
{% endblock %}

tests/express-middleware/500.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Server error
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const assert = require('node:assert')
2+
const http = require('node:http')
3+
const { join } = require('node:path')
4+
const { describe, it, beforeEach, afterEach } = require('node:test')
5+
6+
const express = require('express')
7+
const nunjucks = require('nunjucks')
8+
const request = require('supertest')
9+
10+
const originalError = console.error
11+
let errorCalls = []
12+
13+
const renderErrorPage = require('../../lib/express-middleware/render-error-page')
14+
15+
const app = express()
16+
app.set('view engine', 'html')
17+
nunjucks.configure([join(__dirname, '.')], {
18+
express: app,
19+
noCache: true
20+
})
21+
22+
app.use(function (_req, _res, _next) {
23+
throw Error('Template error')
24+
})
25+
26+
app.use(renderErrorPage)
27+
28+
const server = http.createServer(app)
29+
30+
describe('renderErrorPage', () => {
31+
beforeEach(() => {
32+
errorCalls = []
33+
console.error = (...args) => {
34+
errorCalls.push(args)
35+
originalError(...args) // Still logs to console
36+
}
37+
})
38+
39+
afterEach(() => {
40+
console.error = originalError
41+
})
42+
43+
it('sets the 500 status code', async () => {
44+
const response = await request(server).get('/error')
45+
assert.strictEqual(response.status, 500)
46+
})
47+
48+
it('renders the 500.html template', async () => {
49+
const response = await request(server).get('/error')
50+
assert.strictEqual(response.text, 'Server error\n')
51+
})
52+
53+
it('calls console.error() with the error', async () => {
54+
await request(server).get('/error')
55+
assert.strictEqual(errorCalls.length, 1)
56+
assert.ok(errorCalls[0][0].startsWith('Error: Template error'))
57+
})
58+
})

0 commit comments

Comments
 (0)