Repository: dhsc-govuk/FingertipsNext
Pull request #255: DHSCFT-129 - technical errors page
Author: login: MonkeyDate
Created: 2025-03-11T17:41:58Z | Merged: 2025-03-18T17:34:47Z

# Description

Jira ticket: [DHSCFT-129](https://bjss-enterprise.atlassian.net/browse/DHSCFT-129)

Pages for unexpected/unhandled errors.

## Changes

- Added `global-error` page
- Added `not-found` page
- Bring existing `ErrorPage` design inline with `global-error` page
- Extend existing `try/catch/ErrorPage` mechanism to cover all top level pages
- A couple of minor tweaks to reduce number of warnings in console when running jest unit tests.

## Validation

Manual testing. Unhandled exceptions that occur will only show the expected error page in production builds (`npm run build` plus `npm run start`). In dev builds the default developer focused page is shown. Exceptions caught in the main setup of each root page are caught and the `ErrorPage` content is displayed. Exceptions that occur asynchronously in components withing the page will follow the unhandled exception path mentioned previously.

Playwright UI tests.


Diff:
diff --git a/frontend/fingertips-frontend/app/(home)/page.test.tsx b/frontend/fingertips-frontend/app/(home)/page.test.tsx
index 10fd45573..34568e2dd 100644
--- a/frontend/fingertips-frontend/app/(home)/page.test.tsx
+++ b/frontend/fingertips-frontend/app/(home)/page.test.tsx
@@ -150,20 +150,4 @@ describe('Home page', () => {
       expect(page.props.searchState).toEqual(updatedSearchState);
     });
   });
-
-  describe('Check correct props are passed to the error component when there is an error', () => {
-    it('should pass the correct props when getAreaFilterData call returns an error', async () => {
-      mockGetAreaFilterData.mockRejectedValue('Some areas api error');
-
-      const page = await HomePage({
-        searchParams: generateSearchParams({}),
-      });
-
-      expect(page.props.errorText).toEqual(
-        'An error has been returned by the service. Please try again.'
-      );
-      expect(page.props.errorLink).toEqual('/');
-      expect(page.props.errorLinkText).toEqual('Return to Search');
-    });
-  });
 });
diff --git a/frontend/fingertips-frontend/app/(home)/page.tsx b/frontend/fingertips-frontend/app/(home)/page.tsx
index a3049cdc4..2a0e9226d 100644
--- a/frontend/fingertips-frontend/app/(home)/page.tsx
+++ b/frontend/fingertips-frontend/app/(home)/page.tsx
@@ -77,12 +77,6 @@ export default async function Page(
     );
   } catch (error) {
     console.log(`Error response received from call: ${error}`);
-    return (
-      <ErrorPage
-        errorText="An error has been returned by the service. Please try again."
-        errorLink="/"
-        errorLinkText="Return to Search"
-      />
-    );
+    return <ErrorPage />;
   }
 }
diff --git a/frontend/fingertips-frontend/app/chart/page.tsx b/frontend/fingertips-frontend/app/chart/page.tsx
index 9d64c092a..1f664d8ae 100644
--- a/frontend/fingertips-frontend/app/chart/page.tsx
+++ b/frontend/fingertips-frontend/app/chart/page.tsx
@@ -31,150 +31,159 @@ import { ViewsContext } from '@/components/views/ViewsContext';
 import { SearchServiceFactory } from '@/lib/search/searchServiceFactory';
 import { IndicatorDocument } from '@/lib/search/searchTypes';
 import { getAreaFilterData } from '@/lib/areaFilterHelpers/getAreaFilterData';
+import { ErrorPage } from '@/components/pages/error';
 
 export default async function ChartPage(
   props: Readonly<{
     searchParams?: Promise<SearchStateParams>;
   }>
 ) {
-  const searchParams = await props.searchParams;
-  const stateManager = SearchStateManager.initialise(searchParams);
-  const {
-    [SearchParams.IndicatorsSelected]: indicators,
-    [SearchParams.AreasSelected]: areaCodes,
-    [SearchParams.AreaTypeSelected]: selectedAreaType,
-    [SearchParams.GroupSelected]: selectedGroupCode,
-  } = stateManager.getSearchState();
-
-  const areasSelected = areaCodes ?? [];
-  const indicatorsSelected = indicators ?? [];
-
-  // We don't want to render this page statically
-  await connection();
-
-  const indicatorApi = ApiClientFactory.getIndicatorsApiClient();
-
-  const areaCodesToRequest =
-    selectedGroupCode && selectedGroupCode != areaCodeForEngland
-      ? [...areasSelected, areaCodeForEngland, selectedGroupCode]
-      : [...areasSelected, areaCodeForEngland];
-
-  const healthIndicatorData = await Promise.all(
-    indicatorsSelected.map((indicatorId) =>
-      indicatorApi.getHealthDataForAnIndicator(
-        {
-          indicatorId: Number(indicatorId),
-          areaCodes: areaCodesToRequest,
-          inequalities: shouldDisplayInequalities(
-            indicatorsSelected,
-            areasSelected
-          )
-            ? [GetHealthDataForAnIndicatorInequalitiesEnum.Sex]
-            : [],
-        },
-        API_CACHE_CONFIG
-      )
-    )
-  );
-
-  let rawPopulationData: HealthDataForArea[] | undefined;
   try {
-    rawPopulationData = await indicatorApi.getHealthDataForAnIndicator(
-      {
-        indicatorId: indicatorIdForPopulation,
-        areaCodes: [...areasSelected, areaCodeForEngland],
-        inequalities: ['age', 'sex'],
-      },
-      API_CACHE_CONFIG
-    );
-  } catch (error) {
-    console.log('error getting population data ', error);
-  }
-
-  // Passing the first two areas selected until business logic to select baseline comparator for pop pyramids is added
-  const preparedPopulationData: PopulationData | undefined = rawPopulationData
-    ? preparePopulationData(
-        rawPopulationData,
-        areasSelected[0],
-        areasSelected[1]
+    const searchParams = await props.searchParams;
+    const stateManager = SearchStateManager.initialise(searchParams);
+    const {
+      [SearchParams.IndicatorsSelected]: indicators,
+      [SearchParams.AreasSelected]: areaCodes,
+      [SearchParams.AreaTypeSelected]: selectedAreaType,
+      [SearchParams.GroupSelected]: selectedGroupCode,
+    } = stateManager.getSearchState();
+
+    const areasSelected = areaCodes ?? [];
+    const indicatorsSelected = indicators ?? [];
+
+    // We don't want to render this page statically
+    await connection();
+
+    const indicatorApi = ApiClientFactory.getIndicatorsApiClient();
+
+    const areaCodesToRequest =
+      selectedGroupCode && selectedGroupCode != areaCodeForEngland
+        ? [...areasSelected, areaCodeForEngland, selectedGroupCode]
+        : [...areasSelected, areaCodeForEngland];
+
+    const healthIndicatorData = await Promise.all(
+      indicatorsSelected.map((indicatorId) =>
+        indicatorApi.getHealthDataForAnIndicator(
+          {
+            indicatorId: Number(indicatorId),
+            areaCodes: areaCodesToRequest,
+            inequalities: shouldDisplayInequalities(
+              indicatorsSelected,
+              areasSelected
+            )
+              ? [GetHealthDataForAnIndicatorInequalitiesEnum.Sex]
+              : [],
+          },
+          API_CACHE_CONFIG
+        )
       )
-    : undefined;
+    );
 
-  // only checking for selectedAreaType, single indicator and two or more areas until business logic to also confirm when an entire Group of areas has been selected is in place
-  const mapDataIsRequired =
-    selectedAreaType &&
-    indicatorsSelected.length === 1 &&
-    areasSelected.length >= 2;
+    let rawPopulationData: HealthDataForArea[] | undefined;
+    try {
+      rawPopulationData = await indicatorApi.getHealthDataForAnIndicator(
+        {
+          indicatorId: indicatorIdForPopulation,
+          areaCodes: [...areasSelected, areaCodeForEngland],
+          inequalities: ['age', 'sex'],
+        },
+        API_CACHE_CONFIG
+      );
+    } catch (error) {
+      console.log('error getting population data ', error);
+    }
+
+    // Passing the first two areas selected until business logic to select baseline comparator for pop pyramids is added
+    const preparedPopulationData: PopulationData | undefined = rawPopulationData
+      ? preparePopulationData(
+          rawPopulationData,
+          areasSelected[0],
+          areasSelected[1]
+        )
+      : undefined;
+
+    // only checking for selectedAreaType, single indicator and two or more areas until business logic to also confirm when an entire Group of areas has been selected is in place
+    const mapDataIsRequired =
+      selectedAreaType &&
+      indicatorsSelected.length === 1 &&
+      areasSelected.length >= 2;
+
+    const mapData = mapDataIsRequired
+      ? getMapData(selectedAreaType as AreaTypeKeysForMapMeta, areasSelected)
+      : undefined;
+
+    let indicatorMetadata: IndicatorDocument | undefined;
+    try {
+      indicatorMetadata =
+        await SearchServiceFactory.getIndicatorSearchService().getIndicator(
+          indicatorsSelected[0]
+        );
+    } catch (error) {
+      console.error(
+        'error getting meta data for health indicator for area',
+        error
+      );
+    }
 
-  const mapData = mapDataIsRequired
-    ? getMapData(selectedAreaType as AreaTypeKeysForMapMeta, areasSelected)
-    : undefined;
+    // Area filtering data
+    const areasApi = ApiClientFactory.getAreasApiClient();
 
-  let indicatorMetadata: IndicatorDocument | undefined;
-  try {
-    indicatorMetadata =
-      await SearchServiceFactory.getIndicatorSearchService().getIndicator(
-        indicatorsSelected[0]
+    // set England as areas if all other areas are removed
+    // DHSCFT-481 to futher refine this behaviour
+    if (areasSelected.length === 0) {
+      stateManager.addParamValueToState(
+        SearchParams.AreasSelected,
+        areaCodeForEngland
       );
-  } catch (error) {
-    console.error(
-      'error getting meta data for health indicator for area',
-      error
+    }
+
+    const selectedAreasData =
+      areasSelected && areasSelected.length > 0
+        ? await Promise.all(
+            areasSelected.map((area) =>
+              areasApi.getArea({ areaCode: area }, API_CACHE_CONFIG)
+            )
+          )
+        : [];
+
+    const {
+      availableAreaTypes,
+      availableAreas,
+      availableGroupTypes,
+      availableGroups,
+      updatedSearchState,
+    } = await getAreaFilterData(
+      stateManager.getSearchState(),
+      selectedAreasData
     );
-  }
 
-  // Area filtering data
-  const areasApi = ApiClientFactory.getAreasApiClient();
-
-  // set England as areas if all other areas are removed
-  // DHSCFT-481 to futher refine this behaviour
-  if (areasSelected.length === 0) {
-    stateManager.addParamValueToState(
-      SearchParams.AreasSelected,
-      areaCodeForEngland
+    if (updatedSearchState) {
+      stateManager.setState(updatedSearchState);
+    }
+
+    return (
+      <>
+        <ViewsContext
+          searchState={stateManager.getSearchState()}
+          selectedAreasData={selectedAreasData}
+          areaFilterData={{
+            availableAreaTypes,
+            availableGroupTypes,
+            availableGroups,
+            availableAreas,
+          }}
+        />
+        <Chart
+          populationData={preparedPopulationData}
+          healthIndicatorData={healthIndicatorData}
+          mapData={mapData}
+          searchState={stateManager.getSearchState()}
+          measurementUnit={indicatorMetadata?.unitLabel}
+        />
+      </>
     );
+  } catch (error) {
+    console.log(`Error response received from call: ${error}`);
+    return <ErrorPage />;
   }
-
-  const selectedAreasData =
-    areasSelected && areasSelected.length > 0
-      ? await Promise.all(
-          areasSelected.map((area) =>
-            areasApi.getArea({ areaCode: area }, API_CACHE_CONFIG)
-          )
-        )
-      : [];
-
-  const {
-    availableAreaTypes,
-    availableAreas,
-    availableGroupTypes,
-    availableGroups,
-    updatedSearchState,
-  } = await getAreaFilterData(stateManager.getSearchState(), selectedAreasData);
-
-  if (updatedSearchState) {
-    stateManager.setState(updatedSearchState);
-  }
-
-  return (
-    <>
-      <ViewsContext
-        searchState={stateManager.getSearchState()}
-        selectedAreasData={selectedAreasData}
-        areaFilterData={{
-          availableAreaTypes,
-          availableGroupTypes,
-          availableGroups,
-          availableAreas,
-        }}
-      />
-      <Chart
-        populationData={preparedPopulationData}
-        healthIndicatorData={healthIndicatorData}
-        mapData={mapData}
-        searchState={stateManager.getSearchState()}
-        measurementUnit={indicatorMetadata?.unitLabel}
-      />
-    </>
-  );
 }
diff --git a/frontend/fingertips-frontend/app/global-error.tsx b/frontend/fingertips-frontend/app/global-error.tsx
new file mode 100644
index 000000000..c96bba025
--- /dev/null
+++ b/frontend/fingertips-frontend/app/global-error.tsx
@@ -0,0 +1,28 @@
+'use client'; // Error boundaries must be Client Components
+
+import StyledComponentsRegistry from '@/lib/registry';
+import { FTHeader } from '@/components/molecules/Header';
+import { FTContainer } from '@/components/layouts/container';
+import { FTFooter } from '@/components/molecules/Footer';
+import { ErrorPage } from '@/components/pages/error';
+
+// next.js will only use this page in production builds, meaning that
+// in dev builds you will see the developer oriented unhandled exception
+// page that is useful for debugging
+//
+export default function GlobalError() {
+  return (
+    // global-error must include html and body tags
+    <html lang="en">
+      <body>
+        <StyledComponentsRegistry>
+          <FTHeader />
+          <FTContainer>
+            <ErrorPage />
+          </FTContainer>
+          <FTFooter />
+        </StyledComponentsRegistry>
+      </body>
+    </html>
+  );
+}
diff --git a/frontend/fingertips-frontend/app/indicator/[indicatorId]/page.tsx b/frontend/fingertips-frontend/app/indicator/[indicatorId]/page.tsx
index 43c25515f..9efcd06ea 100644
--- a/frontend/fingertips-frontend/app/indicator/[indicatorId]/page.tsx
+++ b/frontend/fingertips-frontend/app/indicator/[indicatorId]/page.tsx
@@ -5,33 +5,39 @@ import {
 import { SearchServiceFactory } from '@/lib/search/searchServiceFactory';
 import placeholderIndicatorMetadata from '../../../assets/placeholderIndicatorMetadata.json';
 import { redirect } from 'next/navigation';
+import { ErrorPage } from '@/components/pages/error';
 
 export default async function IndicatorDefinitionPage(
   props: Readonly<{
     params: Promise<{ indicatorId: string }>;
   }>
 ) {
-  const { indicatorId } = await props.params;
+  try {
+    const { indicatorId } = await props.params;
 
-  const indicatorMetadata =
-    await SearchServiceFactory.getIndicatorSearchService().getIndicator(
-      indicatorId
-    );
+    const indicatorMetadata =
+      await SearchServiceFactory.getIndicatorSearchService().getIndicator(
+        indicatorId
+      );
 
-  if (indicatorMetadata === undefined) {
-    redirect('/');
-  }
+    if (indicatorMetadata === undefined) {
+      redirect('/');
+    }
 
-  const placeholderMetadata: IndicatorDefinitionProps = {
-    ...placeholderIndicatorMetadata,
-    indicatorID: String(placeholderIndicatorMetadata.indicatorID),
-    lastUpdatedDate: new Date(placeholderIndicatorMetadata.lastUpdatedDate),
-  };
+    const placeholderMetadata: IndicatorDefinitionProps = {
+      ...placeholderIndicatorMetadata,
+      indicatorID: String(placeholderIndicatorMetadata.indicatorID),
+      lastUpdatedDate: new Date(placeholderIndicatorMetadata.lastUpdatedDate),
+    };
 
-  const fullMetadata = {
-    ...placeholderMetadata,
-    ...indicatorMetadata,
-  };
+    const fullMetadata = {
+      ...placeholderMetadata,
+      ...indicatorMetadata,
+    };
 
-  return <IndicatorDefinition indicatorDefinitionProps={fullMetadata} />;
+    return <IndicatorDefinition indicatorDefinitionProps={fullMetadata} />;
+  } catch (error) {
+    console.log(`Error response received from call: ${error}`);
+    return <ErrorPage />;
+  }
 }
diff --git a/frontend/fingertips-frontend/app/not-found.tsx b/frontend/fingertips-frontend/app/not-found.tsx
new file mode 100644
index 000000000..06e2f983e
--- /dev/null
+++ b/frontend/fingertips-frontend/app/not-found.tsx
@@ -0,0 +1,15 @@
+'use client'; // Error boundaries must be Client Components
+
+import { H1, Paragraph } from 'govuk-react';
+
+export default function NotFoundPage() {
+  return (
+    <>
+      <H1 data-testid="error-page-title">Page not found</H1>
+      <Paragraph>If you typed the web address, check it is correct.</Paragraph>
+      <Paragraph>
+        If you pasted the web address, check you copied the entire address.
+      </Paragraph>
+    </>
+  );
+}
diff --git a/frontend/fingertips-frontend/app/results/page.test.tsx b/frontend/fingertips-frontend/app/results/page.test.tsx
index c210c6bef..5aa39692e 100644
--- a/frontend/fingertips-frontend/app/results/page.test.tsx
+++ b/frontend/fingertips-frontend/app/results/page.test.tsx
@@ -259,37 +259,4 @@ describe('Results Page', () => {
       jest.useRealTimers();
     });
   });
-
-  describe('Check correct props to the error component are passed when there is an error', () => {
-    it('should pass the correct props when getAreaFilterData call returns an error', async () => {
-      mockGetAreaFilterData.mockRejectedValue('Some areas api error');
-
-      const page = await ResultsPage({
-        searchParams: generateSearchParams(searchParams),
-      });
-
-      expect(page.props.errorText).toEqual(
-        'An error has been returned by the service. Please try again.'
-      );
-      expect(page.props.errorLink).toEqual('/');
-      expect(page.props.errorLinkText).toEqual('Return to Search');
-    });
-
-    it('should pass the correct props when searchWith returns an error', async () => {
-      mockAreasApi.getAreaTypes.mockResolvedValue(allAreaTypes);
-      mockIndicatorSearchService.searchWith.mockRejectedValue(
-        'Some search-service error'
-      );
-
-      const page = await ResultsPage({
-        searchParams: generateSearchParams(searchParams),
-      });
-
-      expect(page.props.errorText).toEqual(
-        'An error has been returned by the service. Please try again.'
-      );
-      expect(page.props.errorLink).toEqual('/');
-      expect(page.props.errorLinkText).toEqual('Return to Search');
-    });
-  });
 });
diff --git a/frontend/fingertips-frontend/app/results/page.tsx b/frontend/fingertips-frontend/app/results/page.tsx
index 2110635a0..e6328977d 100644
--- a/frontend/fingertips-frontend/app/results/page.tsx
+++ b/frontend/fingertips-frontend/app/results/page.tsx
@@ -88,12 +88,6 @@ export default async function Page(
     );
   } catch (error) {
     console.log(`Error response received from call: ${error}`);
-    return (
-      <ErrorPage
-        errorText="An error has been returned by the service. Please try again."
-        errorLink="/"
-        errorLinkText="Return to Search"
-      />
-    );
+    return <ErrorPage />;
   }
 }
diff --git a/frontend/fingertips-frontend/assets/mockAreaData.json b/frontend/fingertips-frontend/assets/mockAreaData.json
index 02a21d009..df1780cac 100644
--- a/frontend/fingertips-frontend/assets/mockAreaData.json
+++ b/frontend/fingertips-frontend/assets/mockAreaData.json
@@ -163895,5 +163895,22 @@
     "hierarchyType": "NHS",
     "areaType": "GPs",
     "areaTypeCode": null
+  },
+  {
+    "addressLine1": "20 Error House",
+    "addressLine2": "Errorton",
+    "addressLine3": "West Yorkshire",
+    "addressLine4": null,
+    "postcode": "WF99 9AA",
+    "latitude": 0,
+    "longitude": 0,
+    "parentAreas": [],
+    "childAreas": [],
+    "areaCode": "ERROR-AreaCode-1",
+    "areaName": "Error House GP Surgery",
+    "level": 5,
+    "hierarchyType": "NHS",
+    "areaType": "GPs",
+    "areaTypeCode": null
   }
 ]
\ No newline at end of file
diff --git a/frontend/fingertips-frontend/components/pages/error/__snapshots__/error.test.tsx.snap b/frontend/fingertips-frontend/components/pages/error/__snapshots__/error.test.tsx.snap
index 6002abdb2..04b65d031 100644
--- a/frontend/fingertips-frontend/components/pages/error/__snapshots__/error.test.tsx.snap
+++ b/frontend/fingertips-frontend/components/pages/error/__snapshots__/error.test.tsx.snap
@@ -63,8 +63,9 @@ exports[`Error Page snapshot test 1`] = `
 
 <h1
     class="c0"
+    data-testid="error-page-title"
   >
-    An error has occurred.
+    Sorry, there is a problem with the service
   </h1>
   .c0 {
   margin: 0;
@@ -156,41 +157,83 @@ exports[`Error Page snapshot test 1`] = `
     class="c0"
   >
     <p>
-      test error text
+      Try again later.
     </p>
   </div>
-  .c0 {
+  .c1 {
   font-family: "nta",Arial,sans-serif;
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
 }
 
-.c0:focus {
+.c1:focus {
   outline: 3px solid #ffdd00;
   outline-offset: 0;
   background-color: #ffdd00;
 }
 
-.c0:link {
+.c1:link {
   color: #1d70b8;
 }
 
-.c0:visited {
+.c1:visited {
   color: #4c2c92;
 }
 
-.c0:hover {
+.c1:hover {
   color: #2b8cc4;
 }
 
-.c0:active {
+.c1:active {
   color: #2b8cc4;
 }
 
-.c0:focus {
+.c1:focus {
   color: #0b0c0c;
 }
 
+.c0 {
+  margin: 0;
+  font-family: "nta",Arial,sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  font-weight: 400;
+  font-size: 16px;
+  line-height: 1.25;
+  margin-bottom: 15px;
+}
+
+.c0 > p {
+  margin: 0;
+}
+
+.c0 > p > code {
+  padding: 0.2em 0.4em;
+  margin: 0;
+  font-size: 85%;
+  background-color: rgba(27,31,35,0.05);
+  border-radius: 3px;
+}
+
+.c0 > pre {
+  padding: 16px;
+  overflow: auto;
+  font-size: 85%;
+  line-height: 1.45;
+  background-color: #f6f8fa;
+  border-radius: 3px;
+}
+
+.c0 > pre > code {
+  display: inline;
+  padding: 0;
+  margin: 0;
+  border: 0;
+  overflow: visible;
+  line-height: inherit;
+  word-wrap: normal;
+}
+
 @media print {
 
 }
@@ -208,15 +251,15 @@ exports[`Error Page snapshot test 1`] = `
 }
 
 @media print {
-  .c0 {
+  .c1 {
     font-family: sans-serif;
   }
 }
 
 @media print {
-  .c0[href^="/"]::after,
-  .c0[href^="http://"]::after,
-  .c0[href^="https://"]::after {
+  .c1[href^="/"]::after,
+  .c1[href^="http://"]::after,
+  .c1[href^="https://"]::after {
     content: " (" attr(href) ")";
     font-size: 90%;
     word-wrap: break-word;
@@ -224,23 +267,39 @@ exports[`Error Page snapshot test 1`] = `
 }
 
 @media print {
-
+  .c0 {
+    font-size: 14px;
+    line-height: 1.15;
+  }
 }
 
 @media only screen and (min-width:641px) {
-
+  .c0 {
+    font-size: 19px;
+    line-height: 1.3157894736842106;
+  }
 }
 
 @media only screen and (min-width:641px) {
-
+  .c0 {
+    margin-bottom: 20px;
+  }
 }
 
-<a
+<div
     class="c0"
-    data-testid="error-page-link"
-    href="/test-link"
   >
-    test error link text
-  </a>
+    <p>
+      You can 
+      <a
+        class="c1"
+        href="/"
+        node="[object Object]"
+      >
+        go back to the homepage
+      </a>
+       and start your search again.
+    </p>
+  </div>
 </DocumentFragment>
 `;
diff --git a/frontend/fingertips-frontend/components/pages/error/error.test.tsx b/frontend/fingertips-frontend/components/pages/error/error.test.tsx
index 2864650e4..f858b9f62 100644
--- a/frontend/fingertips-frontend/components/pages/error/error.test.tsx
+++ b/frontend/fingertips-frontend/components/pages/error/error.test.tsx
@@ -1,35 +1,10 @@
-import { render, screen } from '@testing-library/react';
+import { render } from '@testing-library/react';
 import { expect } from '@jest/globals';
 import { ErrorPage } from '.';
 
 describe('Error Page', () => {
-  const errorText = 'test error text';
-  const errorLink = '/test-link';
-  const errorLinkText = 'test error link text';
-
-  it('should render elements', () => {
-    render(
-      <ErrorPage
-        errorText={errorText}
-        errorLink={errorLink}
-        errorLinkText={errorLinkText}
-      />
-    );
-
-    expect(screen.getByText(/test error text/i)).toBeInTheDocument();
-    expect(
-      screen.getByRole('link', { name: 'test error link text' })
-    ).toHaveAttribute('href', '/test-link');
-  });
-
   it('snapshot test', () => {
-    const container = render(
-      <ErrorPage
-        errorText={errorText}
-        errorLink={errorLink}
-        errorLinkText={errorLinkText}
-      />
-    );
+    const container = render(<ErrorPage />);
 
     expect(container.asFragment()).toMatchSnapshot();
   });
diff --git a/frontend/fingertips-frontend/components/pages/error/index.tsx b/frontend/fingertips-frontend/components/pages/error/index.tsx
index 1e4a1ff6f..0aa09f825 100644
--- a/frontend/fingertips-frontend/components/pages/error/index.tsx
+++ b/frontend/fingertips-frontend/components/pages/error/index.tsx
@@ -1,25 +1,17 @@
 'use client';
 
-import { H1, Link, Paragraph } from 'govuk-react';
+import { H1, Paragraph } from 'govuk-react';
 
-type ErrorPageProps = {
-  errorText: string;
-  errorLink: string;
-  errorLinkText: string;
-};
-
-export function ErrorPage({
-  errorText,
-  errorLink,
-  errorLinkText,
-}: Readonly<ErrorPageProps>) {
+export function ErrorPage() {
   return (
     <>
-      <H1>An error has occurred.</H1>
-      <Paragraph data-testid="error-page-text">{`${errorText}`}</Paragraph>
-      <Link data-testid="error-page-link" href={`${errorLink}`}>
-        {`${errorLinkText}`}
-      </Link>
+      <H1 data-testid="error-page-title">
+        Sorry, there is a problem with the service
+      </H1>
+      <Paragraph>Try again later.</Paragraph>
+      <Paragraph>
+        You can [go back to the homepage](/) and start your search again.
+      </Paragraph>
     </>
   );
 }
diff --git a/frontend/fingertips-frontend/jest.setup.ts b/frontend/fingertips-frontend/jest.setup.ts
index 3233dae32..6abf9efad 100644
--- a/frontend/fingertips-frontend/jest.setup.ts
+++ b/frontend/fingertips-frontend/jest.setup.ts
@@ -3,3 +3,11 @@ import 'jest-styled-components';
 
 process.env.DHSC_AI_SEARCH_USE_MOCK_SERVICE = 'true';
 process.env.FINGERTIPS_API_URL = 'ft-api-url/';
+
+jest.mock('@/lib/logging', () => {
+  const original = jest.requireActual('@/lib/logging');
+  return {
+    ...original,
+    logUsingMockAiSearchService: jest.fn(),
+  };
+});
diff --git a/frontend/fingertips-frontend/lib/logging.ts b/frontend/fingertips-frontend/lib/logging.ts
new file mode 100644
index 000000000..e92fd6e6a
--- /dev/null
+++ b/frontend/fingertips-frontend/lib/logging.ts
@@ -0,0 +1,3 @@
+export function logUsingMockAiSearchService(user: string): void {
+  console.warn(`${user}: using Mock AI Search Service.`);
+}
diff --git a/frontend/fingertips-frontend/lib/search/areaSearchServiceMock.ts b/frontend/fingertips-frontend/lib/search/areaSearchServiceMock.ts
index df0e3c787..ef951f296 100644
--- a/frontend/fingertips-frontend/lib/search/areaSearchServiceMock.ts
+++ b/frontend/fingertips-frontend/lib/search/areaSearchServiceMock.ts
@@ -1,4 +1,5 @@
 import { IAreaSearchService, AreaDocument } from './searchTypes';
+import { ErrorIdPrefix } from '@/mock/ErrorTriggeringIds';
 
 export class AreaSearchServiceMock implements IAreaSearchService {
   mockAreaData: AreaDocument[];
@@ -19,6 +20,10 @@ export class AreaSearchServiceMock implements IAreaSearchService {
   public async getAreaSuggestions(
     partialAreaName: string
   ): Promise<AreaDocument[]> {
+    if (partialAreaName.startsWith(ErrorIdPrefix)) {
+      throw new Error(`Mock AI Search Service Error - ${partialAreaName}`);
+    }
+
     return this.mockAreaData
       .filter((areaDoc) => {
         return (
diff --git a/frontend/fingertips-frontend/lib/search/searchServiceFactory.ts b/frontend/fingertips-frontend/lib/search/searchServiceFactory.ts
index 6dac8452f..f072f3c1a 100644
--- a/frontend/fingertips-frontend/lib/search/searchServiceFactory.ts
+++ b/frontend/fingertips-frontend/lib/search/searchServiceFactory.ts
@@ -11,6 +11,11 @@ import mockAreaData from '../../assets/mockAreaData.json';
 import mockIndicatorData from '../../assets/mockIndicatorData.json';
 import { IndicatorSearchServiceMock } from './indicatorSearchServiceMock';
 import { readEnvVar, tryReadEnvVar } from '../envUtils';
+import { logUsingMockAiSearchService } from '@/lib/logging';
+
+if (tryReadEnvVar('DHSC_AI_SEARCH_USE_MOCK_SERVICE')) {
+  logUsingMockAiSearchService('SearchServiceFactory');
+}
 
 export class SearchServiceFactory {
   private static readonly DISTRICT_AREA_TYPE_NAME =
@@ -24,7 +29,7 @@ export class SearchServiceFactory {
 
   /*
    * The following code is duplicated between the search-setup project and here.
-   * Both have to make the same updates to the areaData. It is preferrable to make
+   * Both have to make the same updates to the areaData. It is preferable to make
    * this shared code common by creating a shared package but the Typescript,
    * Javascript, Jest and eslint tooling is not currently in place to support this
    *
@@ -68,7 +73,6 @@ export class SearchServiceFactory {
 
   private static buildAreaSearchService(): IAreaSearchService {
     const useMockServer = tryReadEnvVar('DHSC_AI_SEARCH_USE_MOCK_SERVICE');
-    console.log(`buildAreaSearchService: useMockService: ${useMockServer}`);
     return useMockServer === 'true'
       ? this.buildAreaSearchServiceMock(mockAreaData)
       : new AreaSearchService(
@@ -79,9 +83,6 @@ export class SearchServiceFactory {
 
   private static buildIndicatorSearchService(): IIndicatorSearchService {
     const useMockServer = tryReadEnvVar('DHSC_AI_SEARCH_USE_MOCK_SERVICE');
-    console.log(
-      `buildIndicatorSearchService: useMockService: ${useMockServer}`
-    );
     if (useMockServer === 'true') {
       //@ts-expect-error don't care about type checking this json file
       const unparsedIndicatorData = mockIndicatorData as IndicatorDocument[];
diff --git a/frontend/fingertips-frontend/mock/ErrorTriggeringIds.ts b/frontend/fingertips-frontend/mock/ErrorTriggeringIds.ts
new file mode 100644
index 000000000..bedf941c9
--- /dev/null
+++ b/frontend/fingertips-frontend/mock/ErrorTriggeringIds.ts
@@ -0,0 +1,6 @@
+// Special IDs/codes that are included in the mocks/mock-data and exist
+// solely to allow API/AI-Search-Service mocks to trigger error responses.
+
+export const ErrorIdPrefix = 'ERROR';
+export const ErrorAreaCode = `${ErrorIdPrefix}-AreaCode-1`;
+export const ErrorIndicatorId = `${ErrorIdPrefix}-Indicator-1`;
diff --git a/frontend/fingertips-frontend/mock/data/areaData.ts b/frontend/fingertips-frontend/mock/data/areaData.ts
index 745fe797c..333c4b8f7 100644
--- a/frontend/fingertips-frontend/mock/data/areaData.ts
+++ b/frontend/fingertips-frontend/mock/data/areaData.ts
@@ -1,5 +1,8 @@
 import { Area, AreaWithRelations } from '@/generated-sources/ft-api-client';
-import { AreaTypeKeys } from '../../lib/areaFilterHelpers/areaType';
+import {
+  AreaTypeKeys,
+  gpsAreaType,
+} from '../../lib/areaFilterHelpers/areaType';
 import { allEngland, englandArea } from './areas/englandAreas';
 import {
   allRegions,
@@ -44,6 +47,7 @@ import {
   felmoresGP,
   goodingeGP,
 } from './areas/gpsAreas';
+import { ErrorAreaCode } from '@/mock/ErrorTriggeringIds';
 
 export const mockAreaDataForCountry: Record<string, AreaWithRelations> = {
   E92000001: {
@@ -207,6 +211,12 @@ export const mockAreaDataForGP: Record<string, AreaWithRelations> = {
     ...goodingeGP,
     children: [],
   },
+  [`${ErrorAreaCode}`]: {
+    code: `${ErrorAreaCode}`,
+    name: 'Error House GP Surgery',
+    areaType: gpsAreaType,
+    children: [],
+  },
 };
 
 export const mockAreaData: Record<string, AreaWithRelations> = {
diff --git a/frontend/fingertips-frontend/mock/server/handlers.ts b/frontend/fingertips-frontend/mock/server/handlers.ts
index 98732dee1..645a3e5cd 100644
--- a/frontend/fingertips-frontend/mock/server/handlers.ts
+++ b/frontend/fingertips-frontend/mock/server/handlers.ts
@@ -7,6 +7,7 @@ import {
   AreaTypeKeys,
 } from '../../lib/areaFilterHelpers/areaType';
 import { AreaWithRelations } from '@/generated-sources/ft-api-client';
+import { ErrorIdPrefix } from '@/mock/ErrorTriggeringIds';
 
 faker.seed(1);
 
@@ -50,7 +51,6 @@ export const handlers = [
   ),
   http.get(`${baseURL}/areas/:areaCode`, async ({ request, params }) => {
     const areaCode = params.areaCode;
-
     const url = new URL(request.url);
     const includeChildren = url.searchParams.get('include_children') ?? 'false';
     const childAreaType = url.searchParams.get('child_area_type') ?? '';
@@ -58,6 +58,14 @@ export const handlers = [
     if (typeof areaCode !== 'string') {
       return HttpResponse.json({ error: 'Bad request' }, { status: 400 });
     }
+
+    if (areaCode.startsWith(ErrorIdPrefix)) {
+      return HttpResponse.json(
+        { error: `ERROR Scenario ${areaCode}` },
+        { status: 500 }
+      );
+    }
+
     const resultArray = [
       [
         getGetArea200Response(areaCode, includeChildren, childAreaType),
@@ -77,7 +85,18 @@ export const handlers = [
 
     return HttpResponse.json(...resultArray[next() % resultArray.length]);
   }),
-  http.get(`${baseURL}/indicators/:indicatorId`, async () => {
+  http.get(`${baseURL}/indicators/:indicatorId`, async ({ params }) => {
+    const indicatorId = params.indicatorId;
+    if (
+      typeof indicatorId === 'string' &&
+      indicatorId.startsWith(ErrorIdPrefix)
+    ) {
+      return HttpResponse.json(
+        { error: `ERROR Scenario ${params.indicatorId}` },
+        { status: 500 }
+      );
+    }
+
     const resultArray = [[getGetIndicator200Response(), { status: 200 }]];
 
     return HttpResponse.json(...resultArray[next() % resultArray.length]);
@@ -88,6 +107,7 @@ export const handlers = [
       const indicatorId = params.indicatorId;
       const url = new URL(request.url);
       const areaCodes = url.searchParams.getAll('area_codes');
+
       if (typeof indicatorId !== 'string') {
         return HttpResponse.json(getGetHealthDataForAnIndicator400Response(), {
           status: 400,
diff --git a/frontend/fingertips-frontend/playwright.config.ts b/frontend/fingertips-frontend/playwright.config.ts
index ab4eb3d20..6ea582b2f 100644
--- a/frontend/fingertips-frontend/playwright.config.ts
+++ b/frontend/fingertips-frontend/playwright.config.ts
@@ -13,7 +13,7 @@ const config: PlaywrightTestConfig = {
   fullyParallel: true,
   forbidOnly: isCI, // fails the build on CI if you accidentally left test.only in the source code
   retries: isCI ? 1 : 0,
-  workers: isCI ? 2 : undefined,
+  workers: isCI ? 2 : '50%', // 50% of the available CPUs
   timeout: 120_000,
   expect: {
     timeout: 10_000,
diff --git a/frontend/fingertips-frontend/playwright/page-objects/basePage.ts b/frontend/fingertips-frontend/playwright/page-objects/basePage.ts
index af90fa51a..b63bd8536 100644
--- a/frontend/fingertips-frontend/playwright/page-objects/basePage.ts
+++ b/frontend/fingertips-frontend/playwright/page-objects/basePage.ts
@@ -1,7 +1,10 @@
 import type { Page as PlaywrightPage } from 'playwright-core';
 import AxeBuilder from '@axe-core/playwright';
 import { expect } from './pageFactory';
+
 export default class BasePage {
+  readonly errorPageTitleHeaderId = 'error-page-title';
+
   constructor(public readonly page: PlaywrightPage) {}
 
   async waitForURLToContain(containsURL: string) {
@@ -32,4 +35,8 @@ export default class BasePage {
     await this.page.waitForLoadState();
     return pageNavigation;
   }
+
+  errorPageTitle() {
+    return this.page.getByTestId(this.errorPageTitleHeaderId);
+  }
 }
diff --git a/frontend/fingertips-frontend/playwright/page-objects/pageFactory.ts b/frontend/fingertips-frontend/playwright/page-objects/pageFactory.ts
index 0385a8ef1..02c17815e 100644
--- a/frontend/fingertips-frontend/playwright/page-objects/pageFactory.ts
+++ b/frontend/fingertips-frontend/playwright/page-objects/pageFactory.ts
@@ -4,17 +4,40 @@ import HomePage from './pages/homePage';
 import ResultsPage from './pages/resultsPage';
 import ChartPage from './pages/chartPage';
 import { test as baseTest } from '@playwright/test';
+import IndicatorPage from '@/playwright/page-objects/pages/indicatorPage';
 
 type pages = {
   homePage: HomePage;
   resultsPage: ResultsPage;
   chartPage: ChartPage;
+  indicatorPage: IndicatorPage;
 };
 
-const testBase = baseTest.extend<{ axeBuilder: AxeBuilder }>({
+const testBase = baseTest.extend<{
+  axeBuilder: AxeBuilder;
+  failOnUnhandledError: boolean;
+}>({
+  failOnUnhandledError: [true, { option: true }],
+  page: async ({ page, failOnUnhandledError }, use) => {
+    // Console errors
+    page.on('console', (message) => {
+      if (failOnUnhandledError && message.type() === 'error') {
+        throw new Error(`Console error: ${message.text()}`);
+      }
+    });
+
+    // Uncaught exceptions
+    page.on('pageerror', (error) => {
+      if (failOnUnhandledError) {
+        throw new Error(`Page error: ${error.message}`);
+      }
+    });
+
+    await use(page);
+  },
   axeBuilder: [
     async ({ page }, use, testInfo) => {
-      //Initialize an AxeBuilder with the specified accessibility standards
+      // Initialize an AxeBuilder with the specified accessibility standards
       const axeBuilder = new AxeBuilder({ page }).withTags([
         'wcag2a',
         'wcag2aa',
@@ -25,7 +48,7 @@ const testBase = baseTest.extend<{ axeBuilder: AxeBuilder }>({
 
       await use(axeBuilder);
 
-      //Execute the scan
+      // Execute the scan
       const accessibilityResults = await axeBuilder.analyze();
 
       if (accessibilityResults.violations.length > 0) {
@@ -34,14 +57,14 @@ const testBase = baseTest.extend<{ axeBuilder: AxeBuilder }>({
         );
       }
 
-      //Expect no accessibility violations
+      // Expect no accessibility violations
       expect(
         accessibilityResults.violations,
         'Auto-accessibility test failed.'
       ).toEqual([]);
     },
 
-    //Auto is set to false, so accessibility tests will only be executed when we call expectNoAccessibilityViolations(axeBuilder) which is only in the isolated ui tests
+    // auto set to false, so a11y tests only execute when we call expectNoAccessibilityViolations(axeBuilder) which is only in the isolated ui tests
     { scope: 'test', auto: false },
   ],
 });
@@ -56,6 +79,9 @@ export const test = testBase.extend<pages>({
   chartPage: async ({ page }, use) => {
     await use(new ChartPage(page));
   },
+  indicatorPage: async ({ page }, use) => {
+    await use(new IndicatorPage(page));
+  },
 });
 
 export const expect = test.expect;
diff --git a/frontend/fingertips-frontend/playwright/page-objects/pages/homePage.ts b/frontend/fingertips-frontend/playwright/page-objects/pages/homePage.ts
index 8645d4924..052cc083a 100644
--- a/frontend/fingertips-frontend/playwright/page-objects/pages/homePage.ts
+++ b/frontend/fingertips-frontend/playwright/page-objects/pages/homePage.ts
@@ -52,8 +52,12 @@ export default class HomePage extends BasePage {
     await this.page.getByTestId(this.searchButton).click();
   }
 
-  async navigateToHomePage() {
-    await this.navigateTo('/');
+  async navigateToHomePage(queryString?: string) {
+    if (queryString) {
+      await this.navigateTo(`/${queryString}`);
+    } else {
+      await this.navigateTo('/');
+    }
   }
 
   async checkOnHomePage() {
diff --git a/frontend/fingertips-frontend/playwright/page-objects/pages/indicatorPage.ts b/frontend/fingertips-frontend/playwright/page-objects/pages/indicatorPage.ts
new file mode 100644
index 000000000..cee207a76
--- /dev/null
+++ b/frontend/fingertips-frontend/playwright/page-objects/pages/indicatorPage.ts
@@ -0,0 +1,7 @@
+import BasePage from '../basePage';
+
+export default class IndicatorPage extends BasePage {
+  public async navigateToIndicatorPage(indicatorId: string) {
+    await this.navigateTo(`/indicator/${indicatorId}`);
+  }
+}
diff --git a/frontend/fingertips-frontend/playwright/tests/isolated_ui_tests/errorpage.spec.ts b/frontend/fingertips-frontend/playwright/tests/isolated_ui_tests/errorpage.spec.ts
new file mode 100644
index 000000000..a74e8bc44
--- /dev/null
+++ b/frontend/fingertips-frontend/playwright/tests/isolated_ui_tests/errorpage.spec.ts
@@ -0,0 +1,68 @@
+import { test } from '../../page-objects/pageFactory';
+import { ErrorAreaCode, ErrorIndicatorId } from '@/mock/ErrorTriggeringIds';
+
+// we are intentionally setting failOnUnhandledError to false as we are testing error handling in this file
+test.use({ failOnUnhandledError: false });
+
+test.describe('Error page tests', () => {
+  test('Home page displays ErrorPage when API returns unexpected error', async ({
+    homePage,
+    axeBuilder,
+  }) => {
+    await homePage.navigateToHomePage(`?as=${ErrorAreaCode}`);
+
+    await test
+      .expect(homePage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+    await homePage.expectNoAccessibilityViolations(axeBuilder);
+  });
+
+  test('Chart page displays ErrorPage when API returns unexpected error', async ({
+    chartPage,
+    // axeBuilder,
+  }) => {
+    await chartPage.page.goto(`/chart?is=`);
+
+    await test
+      .expect(chartPage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+    // readd once DHSCFT-495 is actioned as the next error overlay is current causing the test to fail
+    // await chartPage.expectNoAccessibilityViolations(axeBuilder);
+  });
+
+  test('Indicator page displays ErrorPage when API returns unexpected error', async ({
+    indicatorPage,
+  }) => {
+    await indicatorPage.navigateToIndicatorPage(`${ErrorIndicatorId}`);
+
+    await test
+      .expect(indicatorPage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Results page displays ErrorPage when API returns unexpected error', async ({
+    resultsPage,
+    axeBuilder,
+  }) => {
+    await resultsPage.navigateToResults(`${ErrorIndicatorId}`, [
+      `${ErrorAreaCode}`,
+    ]);
+
+    await test
+      .expect(resultsPage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+    await resultsPage.expectNoAccessibilityViolations(axeBuilder);
+  });
+
+  test('Navigating to an unsupported url should show the not found page', async ({
+    homePage,
+    axeBuilder,
+  }) => {
+    await homePage.page.goto('cabbage');
+
+    await test
+      .expect(homePage.errorPageTitle())
+      .toContainText('Page not found');
+    await homePage.expectNoAccessibilityViolations(axeBuilder);
+  });
+});


Reviews:
user: login: MonkeyDate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:46:16Z
user: login: MonkeyDate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:46:54Z
user: login: MonkeyDate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T17:36:07Z
user: login: MonkeyDate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T17:36:36Z
user: login: MonkeyDate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T17:37:10Z
user: login: MonkeyDate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T17:37:38Z
user: login: MonkeyDate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T17:38:02Z
user: login: MonkeyDate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T17:38:39Z
user: login: MonkeyDate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T12:58:31Z
user: login: MonkeyDate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T12:59:08Z
user: login: MonkeyDate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T13:00:49Z
user: login: MonkeyDate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T15:53:06Z
user: login: williamwillis11
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T16:20:06Z
user: login: williamwillis11
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T16:22:46Z
user: login: MonkeyDate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T16:37:48Z
user: login: MonkeyDate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T16:38:40Z
user: login: williamwillis11
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T10:32:10Z
user: login: WolfieKnee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T11:39:51Z
user: login: MonkeyDate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:02:18Z
user: login: alibombaye
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T15:39:33Z

Review comments:
diff_hunk: @@ -24,60 +23,46 @@ export default async function Page(
     [SearchParams.AreasSelected]: areasSelected,
   } = stateManager.getSearchState();
 
-  try {
-    await connection();
+  await connection();
path: frontend/fingertips-frontend/app/(home)/page.tsx
user: login: MonkeyDate
body: most of this change is reformatting due to removal of try/catch
created_at: 2025-03-11T17:46:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,70 +24,57 @@ export default async function Page(
     [SearchParams.AreasSelected]: areasSelected,
     [SearchParams.IndicatorsSelected]: indicatorsSelected,
   } = stateManager.getSearchState();
-  try {
-    await connection();
 
-    const areasApi = ApiClientFactory.getAreasApiClient();
+  await connection();
path: frontend/fingertips-frontend/app/results/page.tsx
user: login: MonkeyDate
body: most of this change is reformatting due to removal of try/catch
created_at: 2025-03-11T17:46:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -139,20 +139,4 @@ describe('Home page', () => {
       expect(page.props.searchState).toEqual(updatedSearchState);
     });
   });
-
path: frontend/fingertips-frontend/app/(home)/page.test.tsx
user: login: MonkeyDate
body: will now need to be done in UI tests, if possible
created_at: 2025-03-12T17:36:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,87 +25,93 @@ import {
 } from '@/generated-sources/ft-api-client';
 import { shouldDisplayInequalities } from '@/components/organisms/Inequalities/inequalitiesHelpers';
 import { ViewsContext } from '@/components/views/ViewsContext';
+import { ErrorPage } from '@/components/pages/error';
 
 export default async function ChartPage(
   props: Readonly<{
     searchParams?: Promise<SearchStateParams>;
   }>
 ) {
-  const searchParams = await props.searchParams;
-  const stateManager = SearchStateManager.initialise(searchParams);
-  const {
-    [SearchParams.IndicatorsSelected]: indicators,
-    [SearchParams.AreasSelected]: areaCodes,
-    [SearchParams.AreaTypeSelected]: selectedAreaType,
-    [SearchParams.GroupSelected]: selectedGroupCode,
-  } = stateManager.getSearchState();
+  try {
+    const searchParams = await props.searchParams;
path: frontend/fingertips-frontend/app/chart/page.tsx
user: login: MonkeyDate
body: most of this change is reformatting after wrapping the page in a try/catch
created_at: 2025-03-12T17:36:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,33 +5,39 @@ import {
 import { SearchServiceFactory } from '@/lib/search/searchServiceFactory';
 import placeholderIndicatorMetadata from '../../../assets/placeholderIndicatorMetadata.json';
 import { redirect } from 'next/navigation';
+import { ErrorPage } from '@/components/pages/error';
 
 export default async function IndicatorDefinitionPage(
   props: Readonly<{
     params: Promise<{ indicatorId: string }>;
   }>
 ) {
-  const { indicatorId } = await props.params;
+  try {
path: frontend/fingertips-frontend/app/indicator/[indicatorId]/page.tsx
user: login: MonkeyDate
body: most of this change is reformatting due to wrapping the page in a try/catch
created_at: 2025-03-12T17:37:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -247,37 +247,4 @@ describe('Results Page', () => {
       jest.useRealTimers();
     });
   });
-
path: frontend/fingertips-frontend/app/results/page.test.tsx
user: login: MonkeyDate
body: will now need to be tested in UI tests, if possible
created_at: 2025-03-12T17:37:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,6 +14,12 @@ type ConfidenceIntervalCheckboxProps = {
   searchState: SearchStateParams;
 };
 
path: frontend/fingertips-frontend/components/molecules/ConfidenceIntervalCheckbox/index.tsx
user: login: MonkeyDate
body: moved this to remove warning
created_at: 2025-03-12T17:38:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,14 +22,15 @@ type OneIndicatorOneAreaViewProps = {
   indicatorMetadata?: IndicatorDocument;
 };
 
+const StyledParagraphDataSource = styled(Paragraph)(
path: frontend/fingertips-frontend/components/viewPlots/OneIndicatorOneAreaViewPlots/index.tsx
user: login: MonkeyDate
body: moved this to remove a warning
created_at: 2025-03-12T17:38:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,6 +12,12 @@ import mockIndicatorData from '../../assets/mockIndicatorData.json';
 import { IndicatorSearchServiceMock } from './indicatorSearchServiceMock';
 import { readEnvVar, tryReadEnvVar } from '../envUtils';
 
+if (tryReadEnvVar('DHSC_AI_SEARCH_USE_MOCK_SERVICE') && !tryReadEnvVar('JEST_WORKER_ID')) {
path: frontend/fingertips-frontend/lib/search/searchServiceFactory.ts
user: login: MonkeyDate
body: related to reducing noise in jest unit tests.

If we are using the mock service, and we're not a unit tests, put a warning in the console.
created_at: 2025-03-13T12:58:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,7 +74,7 @@ export class SearchServiceFactory {
 
   private static buildAreaSearchService(): IAreaSearchService {
     const useMockServer = tryReadEnvVar('DHSC_AI_SEARCH_USE_MOCK_SERVICE');
-    console.log(`buildAreaSearchService: useMockService: ${useMockServer}`);
+    console.debug(`buildAreaSearchService: useMockService: ${useMockServer}`);
path: frontend/fingertips-frontend/lib/search/searchServiceFactory.ts
user: login: MonkeyDate
body: might be possible to remove these without affecting anyone
created_at: 2025-03-13T12:59:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,3 +3,5 @@ import 'jest-styled-components';
 
 process.env.DHSC_AI_SEARCH_USE_MOCK_SERVICE = 'true';
 process.env.FINGERTIPS_API_URL = 'ft-api-url/';
+
+jest.spyOn(console, "debug").mockImplementation(() => {});
path: frontend/fingertips-frontend/jest.setup.ts
user: login: MonkeyDate
body: related to unit test noise reduction

other code can use console.debug to output logging we either won't see or wont care about in production. We mute that output in jest tests to keep the noise down.
created_at: 2025-03-13T13:00:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,77 @@
+import { test } from '../../page-objects/pageFactory';
+import { server } from '@/mock/server/node';
+const subjectSearchTerm = 'hospital';
+
+test.beforeAll(
+  `get indicatorIDs from the mock data source for searchTerm: ${subjectSearchTerm} and get mock area data`,
+  () => {}
+);
+
+test.describe('Error page tests', () => {
+  test('Home page displays ErrorPage when API returns unexpected error', async ({
+    homePage,
+  }) => {
+    await homePage.navigateToHomePage('?as=ERRORY07275');
+
+    await test
+      .expect(homePage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Chart page displays ErrorPage when API returns unexpected error', async ({
path: frontend/fingertips-frontend/playwright/tests/isolated_ui_tests/errorpage.spec.ts
user: login: MonkeyDate
body: this test doesn't always work. depends how the test is run. often fails in a way that indicates the page url has no query parameters
created_at: 2025-03-14T15:53:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,77 @@
+import { test } from '../../page-objects/pageFactory';
+import { server } from '@/mock/server/node';
+const subjectSearchTerm = 'hospital';
+
+test.beforeAll(
+  `get indicatorIDs from the mock data source for searchTerm: ${subjectSearchTerm} and get mock area data`,
+  () => {}
+);
+
+test.describe('Error page tests', () => {
+  test('Home page displays ErrorPage when API returns unexpected error', async ({
+    homePage,
+  }) => {
+    await homePage.navigateToHomePage('?as=ERRORY07275');
+
+    await test
+      .expect(homePage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Chart page displays ErrorPage when API returns unexpected error', async ({
path: frontend/fingertips-frontend/playwright/tests/isolated_ui_tests/errorpage.spec.ts
user: login: williamwillis11
body: checking this is no longer the case?
created_at: 2025-03-17T16:20:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,74 @@
+import { test } from '../../page-objects/pageFactory';
+import { server } from '@/mock/server/node';
+import { ErrorAreaCode, ErrorIndicatorId } from '@/mock/ErrorTriggeringIds';
+
+test.describe('Error page tests', () => {
+  test('Home page displays ErrorPage when API returns unexpected error', async ({
+    homePage,
+  }) => {
+    await homePage.navigateToHomePage(`?as=${ErrorAreaCode}`);
+
+    await test
+      .expect(homePage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Chart page displays ErrorPage when API returns unexpected error', async ({
+    chartPage,
+  }) => {
+    await chartPage.page.goto(`/chart?is=${ErrorIndicatorId}`);
+
+    await test
+      .expect(chartPage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Indicator page displays ErrorPage when API returns unexpected error', async ({
+    indicatorPage,
+  }) => {
+    await indicatorPage.navigateToIndicatorPage(`${ErrorIndicatorId}`);
+
+    await test
+      .expect(indicatorPage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Results page displays ErrorPage when API returns unexpected error', async ({
+    resultsPage,
+  }) => {
+    await resultsPage.navigateToResults(`${ErrorIndicatorId}`, [
+      `${ErrorAreaCode}`,
+    ]);
+
+    await test
+      .expect(resultsPage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Navigating to an unsupported url should show the not found page', async ({
+    homePage,
+  }) => {
+    await homePage.page.goto('cabbage');
+
+    await test
+      .expect(homePage.errorPageTitle())
+      .toContainText('Page not found');
+  });
+});
+
+// log out current url when a test fails
+test.afterEach(async ({ page }, testInfo) => {
+  if (testInfo.status !== testInfo.expectedStatus) {
+    // Test failed - capture the URL
+    const url = page.url();
+    console.log(`Test failed! Current URL: ${url}`);
+
+    // You can also attach it to the test report
+    await testInfo.attach('failed-url', {
+      body: url,
+      contentType: 'text/plain',
+    });
+  }
+
+  server.resetHandlers();
path: frontend/fingertips-frontend/playwright/tests/isolated_ui_tests/errorpage.spec.ts
user: login: williamwillis11
body: do we need this? @alibombaye is this expected to be needed when using MSW?
created_at: 2025-03-17T16:22:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,77 @@
+import { test } from '../../page-objects/pageFactory';
+import { server } from '@/mock/server/node';
+const subjectSearchTerm = 'hospital';
+
+test.beforeAll(
+  `get indicatorIDs from the mock data source for searchTerm: ${subjectSearchTerm} and get mock area data`,
+  () => {}
+);
+
+test.describe('Error page tests', () => {
+  test('Home page displays ErrorPage when API returns unexpected error', async ({
+    homePage,
+  }) => {
+    await homePage.navigateToHomePage('?as=ERRORY07275');
+
+    await test
+      .expect(homePage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Chart page displays ErrorPage when API returns unexpected error', async ({
path: frontend/fingertips-frontend/playwright/tests/isolated_ui_tests/errorpage.spec.ts
user: login: MonkeyDate
body: seems to be fine now, provided you run a production type app (npm run build + npm run start).
created_at: 2025-03-17T16:37:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,74 @@
+import { test } from '../../page-objects/pageFactory';
+import { server } from '@/mock/server/node';
+import { ErrorAreaCode, ErrorIndicatorId } from '@/mock/ErrorTriggeringIds';
+
+test.describe('Error page tests', () => {
+  test('Home page displays ErrorPage when API returns unexpected error', async ({
+    homePage,
+  }) => {
+    await homePage.navigateToHomePage(`?as=${ErrorAreaCode}`);
+
+    await test
+      .expect(homePage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Chart page displays ErrorPage when API returns unexpected error', async ({
+    chartPage,
+  }) => {
+    await chartPage.page.goto(`/chart?is=${ErrorIndicatorId}`);
+
+    await test
+      .expect(chartPage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Indicator page displays ErrorPage when API returns unexpected error', async ({
+    indicatorPage,
+  }) => {
+    await indicatorPage.navigateToIndicatorPage(`${ErrorIndicatorId}`);
+
+    await test
+      .expect(indicatorPage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Results page displays ErrorPage when API returns unexpected error', async ({
+    resultsPage,
+  }) => {
+    await resultsPage.navigateToResults(`${ErrorIndicatorId}`, [
+      `${ErrorAreaCode}`,
+    ]);
+
+    await test
+      .expect(resultsPage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Navigating to an unsupported url should show the not found page', async ({
+    homePage,
+  }) => {
+    await homePage.page.goto('cabbage');
+
+    await test
+      .expect(homePage.errorPageTitle())
+      .toContainText('Page not found');
+  });
+});
+
+// log out current url when a test fails
+test.afterEach(async ({ page }, testInfo) => {
+  if (testInfo.status !== testInfo.expectedStatus) {
+    // Test failed - capture the URL
+    const url = page.url();
+    console.log(`Test failed! Current URL: ${url}`);
+
+    // You can also attach it to the test report
+    await testInfo.attach('failed-url', {
+      body: url,
+      contentType: 'text/plain',
+    });
+  }
+
+  server.resetHandlers();
path: frontend/fingertips-frontend/playwright/tests/isolated_ui_tests/errorpage.spec.ts
user: login: MonkeyDate
body: I copied that from the other isolated UI tests file.
created_at: 2025-03-17T16:38:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,74 @@
+import { test } from '../../page-objects/pageFactory';
+import { server } from '@/mock/server/node';
+import { ErrorAreaCode, ErrorIndicatorId } from '@/mock/ErrorTriggeringIds';
+
+test.describe('Error page tests', () => {
+  test('Home page displays ErrorPage when API returns unexpected error', async ({
+    homePage,
+  }) => {
+    await homePage.navigateToHomePage(`?as=${ErrorAreaCode}`);
+
+    await test
+      .expect(homePage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Chart page displays ErrorPage when API returns unexpected error', async ({
+    chartPage,
+  }) => {
+    await chartPage.page.goto(`/chart?is=${ErrorIndicatorId}`);
+
+    await test
+      .expect(chartPage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Indicator page displays ErrorPage when API returns unexpected error', async ({
+    indicatorPage,
+  }) => {
+    await indicatorPage.navigateToIndicatorPage(`${ErrorIndicatorId}`);
+
+    await test
+      .expect(indicatorPage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Results page displays ErrorPage when API returns unexpected error', async ({
+    resultsPage,
+  }) => {
+    await resultsPage.navigateToResults(`${ErrorIndicatorId}`, [
+      `${ErrorAreaCode}`,
+    ]);
+
+    await test
+      .expect(resultsPage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Navigating to an unsupported url should show the not found page', async ({
+    homePage,
+  }) => {
+    await homePage.page.goto('cabbage');
+
+    await test
+      .expect(homePage.errorPageTitle())
+      .toContainText('Page not found');
+  });
+});
+
+// log out current url when a test fails
+test.afterEach(async ({ page }, testInfo) => {
+  if (testInfo.status !== testInfo.expectedStatus) {
+    // Test failed - capture the URL
+    const url = page.url();
+    console.log(`Test failed! Current URL: ${url}`);
+
+    // You can also attach it to the test report
+    await testInfo.attach('failed-url', {
+      body: url,
+      contentType: 'text/plain',
+    });
+  }
+
+  server.resetHandlers();
path: frontend/fingertips-frontend/playwright/tests/isolated_ui_tests/errorpage.spec.ts
user: login: williamwillis11
body: the only other isolated ui test file is

frontend/fingertips-frontend/playwright/tests/isolated_ui_tests/navigation_and_validation.spec.ts

and that doesnt have it? i cant find resetHandlers anywhere else in the codebase


created_at: 2025-03-18T10:32:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,6 +25,9 @@ let allIndicatorIDs: string[];
 let filteredIndicatorIds: string[];
 let allNHSRegionAreas: AreaDocument[];
 
+// remove this in bugfix DHSCFT-482
path: frontend/fingertips-frontend/playwright/tests/isolated_ui_tests/navigation_and_validation.spec.ts
user: login: WolfieKnee
body: DHSCFT-482 now merged so this can be removed
created_at: 2025-03-18T11:39:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,74 @@
+import { test } from '../../page-objects/pageFactory';
+import { server } from '@/mock/server/node';
+import { ErrorAreaCode, ErrorIndicatorId } from '@/mock/ErrorTriggeringIds';
+
+test.describe('Error page tests', () => {
+  test('Home page displays ErrorPage when API returns unexpected error', async ({
+    homePage,
+  }) => {
+    await homePage.navigateToHomePage(`?as=${ErrorAreaCode}`);
+
+    await test
+      .expect(homePage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Chart page displays ErrorPage when API returns unexpected error', async ({
+    chartPage,
+  }) => {
+    await chartPage.page.goto(`/chart?is=${ErrorIndicatorId}`);
+
+    await test
+      .expect(chartPage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Indicator page displays ErrorPage when API returns unexpected error', async ({
+    indicatorPage,
+  }) => {
+    await indicatorPage.navigateToIndicatorPage(`${ErrorIndicatorId}`);
+
+    await test
+      .expect(indicatorPage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Results page displays ErrorPage when API returns unexpected error', async ({
+    resultsPage,
+  }) => {
+    await resultsPage.navigateToResults(`${ErrorIndicatorId}`, [
+      `${ErrorAreaCode}`,
+    ]);
+
+    await test
+      .expect(resultsPage.errorPageTitle())
+      .toContainText('Sorry, there is a problem with the service');
+  });
+
+  test('Navigating to an unsupported url should show the not found page', async ({
+    homePage,
+  }) => {
+    await homePage.page.goto('cabbage');
+
+    await test
+      .expect(homePage.errorPageTitle())
+      .toContainText('Page not found');
+  });
+});
+
+// log out current url when a test fails
+test.afterEach(async ({ page }, testInfo) => {
+  if (testInfo.status !== testInfo.expectedStatus) {
+    // Test failed - capture the URL
+    const url = page.url();
+    console.log(`Test failed! Current URL: ${url}`);
+
+    // You can also attach it to the test report
+    await testInfo.attach('failed-url', {
+      body: url,
+      contentType: 'text/plain',
+    });
+  }
+
+  server.resetHandlers();
path: frontend/fingertips-frontend/playwright/tests/isolated_ui_tests/errorpage.spec.ts
user: login: MonkeyDate
body: guess it can go too then.
created_at: 2025-03-18T14:02:18Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-18T17:11:19Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=dhsc-govuk_FingertipsNext&pullRequest=255) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=dhsc-govuk_FingertipsNext&pullRequest=255&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=dhsc-govuk_FingertipsNext&pullRequest=255&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=dhsc-govuk_FingertipsNext&pullRequest=255&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=dhsc-govuk_FingertipsNext&pullRequest=255&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=dhsc-govuk_FingertipsNext&pullRequest=255&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=dhsc-govuk_FingertipsNext&pullRequest=255)


author_association: NONE