diff --git a/changelog/entries/unreleased/bug/3331_database_enhance_backend_errors_handling_in_ui.json b/changelog/entries/unreleased/bug/3331_database_enhance_backend_errors_handling_in_ui.json
new file mode 100644
index 000000000..477ff7eb1
--- /dev/null
+++ b/changelog/entries/unreleased/bug/3331_database_enhance_backend_errors_handling_in_ui.json
@@ -0,0 +1,7 @@
+{
+    "type": "bug",
+    "message": "Database: when an error is returned by the backend, it will be shown inside of the body of the view, so user still have access to navigation interface.",
+    "issue_number": 3331,
+    "bullet_points": [],
+    "created_at": "2025-01-31"
+}
diff --git a/web-frontend/modules/database/components/table/Table.vue b/web-frontend/modules/database/components/table/Table.vue
index e82da0345..dbe7f6753 100644
--- a/web-frontend/modules/database/components/table/Table.vue
+++ b/web-frontend/modules/database/components/table/Table.vue
@@ -210,9 +210,10 @@
       />
     </header>
     <div class="layout__col-2-2 content">
+      <DefaultErrorPage v-if="viewError" :error="viewError" />
       <component
         :is="getViewComponent(view)"
-        v-if="hasSelectedView && !tableLoading"
+        v-if="hasSelectedView && !tableLoading && !viewError"
         ref="view"
         :database="database"
         :table="table"
@@ -252,7 +253,8 @@ import ViewSearch from '@baserow/modules/database/components/view/ViewSearch'
 import EditableViewName from '@baserow/modules/database/components/view/EditableViewName'
 import ShareViewLink from '@baserow/modules/database/components/view/ShareViewLink'
 import ExternalLinkBaserowLogo from '@baserow/modules/core/components/ExternalLinkBaserowLogo'
-import ViewGroupBy from '@baserow/modules/database/components/view/ViewGroupBy.vue'
+import ViewGroupBy from '@baserow/modules/database/components/view/ViewGroupBy'
+import DefaultErrorPage from '@baserow/modules/core/components/DefaultErrorPage'
 
 /**
  * This page component is the skeleton for a table. Depending on the selected view it
@@ -260,6 +262,7 @@ import ViewGroupBy from '@baserow/modules/database/components/view/ViewGroupBy.v
  */
 export default {
   components: {
+    DefaultErrorPage,
     ViewGroupBy,
     ExternalLinkBaserowLogo,
     ShareViewLink,
@@ -300,6 +303,14 @@ export default {
       required: true,
       validator: (prop) => typeof prop === 'object' || prop === undefined,
     },
+    viewError: {
+      required: false,
+      validator: (prop) =>
+        typeof prop === 'object' ||
+        typeof prop === 'function' ||
+        prop === undefined,
+      default: null,
+    },
     tableLoading: {
       type: Boolean,
       required: true,
diff --git a/web-frontend/modules/database/pages/table.vue b/web-frontend/modules/database/pages/table.vue
index 2cb6fa162..faa509e86 100644
--- a/web-frontend/modules/database/pages/table.vue
+++ b/web-frontend/modules/database/pages/table.vue
@@ -1,11 +1,14 @@
 <template>
   <div>
+    <DefaultErrorPage v-if="error && !view" :error="error" />
     <Table
+      v-else
       :database="database"
       :table="table"
       :fields="fields"
       :views="views"
       :view="view"
+      :view-error="error"
       :table-loading="tableLoading"
       store-prefix="page/"
       @selected-view="selectedView"
@@ -27,13 +30,15 @@ import { mapState } from 'vuex'
 import Table from '@baserow/modules/database/components/table/Table'
 import { StoreItemLookupError } from '@baserow/modules/core/errors'
 import { getDefaultView } from '@baserow/modules/database/utils/view'
+import DefaultErrorPage from '@baserow/modules/core/components/DefaultErrorPage'
+import { normalizeError } from '@baserow/modules/database/utils/errors'
 
 /**
  * This page component is the skeleton for a table. Depending on the selected view it
  * will load the correct components into the header and body.
  */
 export default {
-  components: { Table },
+  components: { DefaultErrorPage, Table },
   /**
    * When the user leaves to another page we want to unselect the selected table. This
    * way it will not be highlighted the left sidebar.
@@ -56,6 +61,7 @@ export default {
     function parseIntOrNull(x) {
       return x != null ? parseInt(x) : null
     }
+
     const currentRowId = parseIntOrNull(to.params?.rowId)
     const currentTableId = parseIntOrNull(to.params.tableId)
 
@@ -121,32 +127,39 @@ export default {
     const databaseId = parseInt(params.databaseId)
     const tableId = parseInt(params.tableId)
     const viewId = params.viewId ? parseInt(params.viewId) : null
-    const data = {}
-
+    // let's use undefined for view, as it's explicitly checked in components
+    const data = { error: null, view: undefined, fields: null }
     // Try to find the table in the already fetched applications by the
     // workspacesAndApplications middleware and select that one. By selecting the table, the
     // fields and views are also going to be fetched.
     try {
-      const { database, table } = await store.dispatch('table/selectById', {
-        databaseId,
-        tableId,
-      })
+      const { database, table, error } = await store.dispatch(
+        'table/selectById',
+        {
+          databaseId,
+          tableId,
+        }
+      )
       await store.dispatch('workspace/selectById', database.workspace.id)
       data.database = database
       data.table = table
+
+      if (error) {
+        data.error = normalizeError(error)
+        return data
+      }
     } catch (e) {
       // In case of a network error we want to fail hard.
       if (e.response === undefined && !(e instanceof StoreItemLookupError)) {
         throw e
       }
-
-      return error({ statusCode: 404, message: 'Table not found.' })
+      data.error = normalizeError(e)
+      return data
     }
 
     // After selecting the table the fields become available which need to be added to
     // the data.
     data.fields = store.getters['field/getAll']
-    data.view = undefined
 
     // Without a viewId, redirect the user to the default or the first available view.
     if (viewId === null) {
@@ -192,18 +205,17 @@ export default {
         if (e.response === undefined && !(e instanceof StoreItemLookupError)) {
           throw e
         }
+        data.error = normalizeError(e)
 
-        return error({ statusCode: 404, message: 'View not found.' })
+        return data
       }
     }
-
     if (params.rowId) {
       await store.dispatch('rowModalNavigation/fetchRow', {
         tableId,
         rowId: params.rowId,
       })
     }
-
     return data
   },
   head() {
diff --git a/web-frontend/modules/database/store/table.js b/web-frontend/modules/database/store/table.js
index b10ab1c88..5dfed2c3c 100644
--- a/web-frontend/modules/database/store/table.js
+++ b/web-frontend/modules/database/store/table.js
@@ -211,14 +211,18 @@ export const actions = {
     if (getters.getSelectedId === table.id) {
       return { database, table }
     }
-
-    await axios.all([
-      dispatch('view/fetchAll', table, { root: true }),
-      dispatch('field/fetchAll', table, { root: true }),
-    ])
+    let error = null
+    await axios
+      .all([
+        dispatch('view/fetchAll', table, { root: true }),
+        dispatch('field/fetchAll', table, { root: true }),
+      ])
+      .catch((err) => {
+        error = err
+      })
     await dispatch('application/clearChildrenSelected', null, { root: true })
     await dispatch('forceSelect', { database, table })
-    return { database, table }
+    return { database, table, error }
   },
   forceSelect({ commit, dispatch }, { database, table }) {
     dispatch(
@@ -259,7 +263,7 @@ export const actions = {
     }
     const table = database.tables[index]
 
-    return dispatch('select', { database, table })
+    return await dispatch('select', { database, table })
   },
   /**
    * Unselect the selected table.
diff --git a/web-frontend/modules/database/utils/errors.js b/web-frontend/modules/database/utils/errors.js
new file mode 100644
index 000000000..e98d20d2e
--- /dev/null
+++ b/web-frontend/modules/database/utils/errors.js
@@ -0,0 +1,23 @@
+/**
+ * Returns most expected error structure.
+ *
+ * When an error is thrown, it can be of any type. This function tries to return
+ * the most useful error data from the error. It tries to return any first of the
+ * following:
+ *
+ * * http response body, if it's a DRF error structure
+ * * http response object
+ * * the error as-is in any other case
+ *
+ * @param err
+ * @param errorMap
+ * @returns {*}
+ */
+export function normalizeError(err) {
+  err = err.response?.data?.message ? err.response.data : err.response || err
+  return {
+    message: err.message,
+    content: err.detail,
+    statusCode: err.statusCode,
+  }
+}
diff --git a/web-frontend/test/unit/database/__snapshots__/publicView.spec.js.snap b/web-frontend/test/unit/database/__snapshots__/publicView.spec.js.snap
index ba6e01a66..555f5b2b8 100644
--- a/web-frontend/test/unit/database/__snapshots__/publicView.spec.js.snap
+++ b/web-frontend/test/unit/database/__snapshots__/publicView.spec.js.snap
@@ -354,6 +354,8 @@ exports[`Public View Page Tests Can see a publicly shared grid view 1`] = `
       <div
         class="layout__col-2-2 content"
       >
+        <!---->
+         
         <div
           class="grid-view grid-view--row-height-undefined"
         >
diff --git a/web-frontend/test/unit/database/view.spec.js b/web-frontend/test/unit/database/view.spec.js
index e4d265ea1..dee817808 100644
--- a/web-frontend/test/unit/database/view.spec.js
+++ b/web-frontend/test/unit/database/view.spec.js
@@ -361,6 +361,114 @@ describe('View Tests', () => {
     expect(updatedCookieValue.length).toBeLessThan(originalDataLength)
   })
 
+  test('Unknown error during views loading is displayed correctly - no view toolbar', async () => {
+    const viewsError = { statusCode: 500, data: 'some backend error' }
+
+    // no list of views
+    const { application, table } = await givenATableWithError({
+      viewsError,
+    })
+    const tableComponent = await testApp.mount(Table, {
+      asyncDataParams: {
+        databaseId: application.id,
+        tableId: table.id,
+        viewId: '123',
+      },
+    })
+
+    expect(tableComponent.vm.views).toEqual([])
+
+    expect(tableComponent.vm.error).toBeTruthy()
+
+    // no table header (view selection, filters, sorting, grouping...)
+    expect(tableComponent.find('header .header__filter-link').exists()).toBe(
+      false
+    )
+
+    expect(tableComponent.find('.placeholder__title').exists()).toBe(true)
+    // error message will be processed and replaced
+    expect(tableComponent.find('.placeholder__title').text()).toBe(
+      'errorLayout.wrong'
+    )
+    expect(tableComponent.find('.placeholder__content').exists()).toBe(true)
+
+    expect(tableComponent.find('.placeholder__content').text()).toBe(
+      'errorLayout.error'
+    )
+  })
+
+  test('API error during views loading is displayed correctly', async () => {
+    const viewsError = {
+      statusCode: 400,
+      data: {
+        message: "The view filter type INVALID doesn't exist.",
+      },
+    }
+
+    // no list of views
+    const { application, table } = await givenATableWithError({
+      viewsError,
+    })
+    const tableComponent = await testApp.mount(Table, {
+      asyncDataParams: {
+        databaseId: application.id,
+        tableId: table.id,
+        viewId: '123',
+      },
+    })
+
+    expect(tableComponent.vm.views).toEqual([])
+    expect(tableComponent.vm.error).toBeTruthy()
+
+    expect(tableComponent.find('header .header__filter-link').exists()).toBe(
+      false
+    )
+    expect(tableComponent.find('.placeholder__title').exists()).toBe(true)
+    expect(tableComponent.find('.placeholder__title').text()).toEqual(
+      viewsError.data.message
+    )
+    expect(tableComponent.find('.placeholder__content').exists()).toBe(true)
+
+    expect(tableComponent.find('.placeholder__content').text()).toEqual(
+      'errorLayout.error'
+    )
+  })
+
+  test('API error during view rows loading', async () => {
+    const rowsError = { statusCode: 500, data: { message: 'Unknown error' } }
+
+    // views list readable, fields readable, rows not readable
+    const { application, table, view } = await givenATableWithError({
+      rowsError,
+    })
+
+    //
+    const tableComponent = await testApp.mount(Table, {
+      asyncDataParams: {
+        databaseId: application.id,
+        tableId: table.id,
+        viewId: view.id,
+      },
+    })
+
+    expect(tableComponent.vm.views).toMatchObject([view])
+
+    // we're past views api call, so the table (with the error) and toolbar should be present
+    expect(tableComponent.find('.header__filter-link').exists()).toBe(true)
+
+    expect(tableComponent.vm.error).toBeTruthy()
+
+    expect(tableComponent.find('.placeholder__title').exists()).toBe(true)
+    expect(tableComponent.find('.placeholder__title').text()).toEqual(
+      rowsError.data.message
+    )
+    expect(tableComponent.find('.placeholder__content').exists()).toBe(true)
+
+    expect(tableComponent.find('.placeholder__content').text()).toEqual(
+      'errorLayout.error'
+    )
+  })
+
   async function givenATableInTheServerWithMultipleViews() {
     const table = mockServer.createTable()
     const { application } = await mockServer.createAppAndWorkspace(table)
@@ -494,4 +602,83 @@ describe('View Tests', () => {
     tables.push(secondTable)
     return { application, tables, views }
   }
+
+  async function givenATableWithError({ viewsError, fieldsError, rowsError }) {
+    const table = mockServer.createTable()
+    // we expect some endpoints to return errors
+    testApp.dontFailOnErrorResponses()
+    const { application } =
+      await mockServer.createAppAndWorkspaceWithMultipleTables([table])
+    const viewId = 1
+
+    const rawGridView = {
+      id: viewId,
+      table_id: table.id,
+      name: `mock_view_${viewId}`,
+      order: 0,
+      type: 'grid',
+      table: {
+        id: table.id,
+        name: table.name,
+        order: 0,
+        database_id: application.id,
+      },
+      filter_type: 'AND',
+      filters_disabled: false,
+      public: null,
+      row_identifier_type: 'id',
+      row_height_size: 'small',
+      filters: [],
+      sortings: [],
+      group_bys: [],
+      decorations: [],
+    }
+
+    const rawFields = [
+      {
+        name: 'Name',
+        type: 'text',
+        primary: true,
+      },
+      {
+        name: 'Last name',
+        type: 'text',
+      },
+      {
+        name: 'Notes',
+        type: 'long_text',
+      },
+      {
+        name: 'Active',
+        type: 'boolean',
+      },
+    ]
+    const rawRows = [
+      {
+        id: 1,
+        order: 0,
+        field_1: 'name',
+        field_2: 'last_name',
+        field_3: 'notes',
+        field_4: false,
+      },
+    ]
+
+    mockServer.mock
+      .onGet(`/database/views/table/${table.id}/`)
+      .replyOnce(
+        viewsError?.statusCode || 200,
+        viewsError?.data || [rawGridView]
+      )
+
+    mockServer.mock
+      .onGet(`/database/fields/table/${table.id}/`)
+      .replyOnce(fieldsError?.statusCode || 200, fieldsError?.data || rawFields)
+
+    mockServer.mock
+      .onGet(`database/views/grid/${rawGridView.id}/`)
+      .replyOnce(rowsError?.statusCode || 200, rowsError?.data || rawRows)
+
+    return { application, table, view: rawGridView }
+  }
 })