diff --git a/client/src/app/editor/editor.component.ts b/client/src/app/editor/editor.component.ts index 5ff7feb..74d2ae8 100644 --- a/client/src/app/editor/editor.component.ts +++ b/client/src/app/editor/editor.component.ts @@ -3524,14 +3524,24 @@ export class EditorComponent // A synchronous throw inside initSetup (called from the .then() // above) lands here too, not just a network failure - log it, or // the table just renders hidden with no clue why. - // eslint-disable-next-line no-console - console.error('editors/getdata failed:', err) - this.getdataError = true - this.tableTrue = true + this.handleGetdataFailure(err) }) } } + /** + * Puts the page into its getdata error state. Shared by the callService + * .catch and initSetup's own retry branch: a throw from the retry runs on a + * bare setTimeout, outside that promise chain, so it would otherwise never + * reach the .catch and the user would be left on the loading spinner. + */ + private handleGetdataFailure(err: any) { + // eslint-disable-next-line no-console + console.error('editors/getdata failed:', err) + this.getdataError = true + this.tableTrue = true + } + /** * VA mode, initial (unfiltered) route only: briefly wait for VA's buffered * first message and, if it already carries a filter, save that filter and @@ -3880,9 +3890,17 @@ export class EditorComponent this.hotInstance = this.hotTableComponent?.hotInstance! if (!this.hotInstance) { - // Retry init, don't permanently abort + // Retry init, don't permanently abort. A throw from the retry runs on a + // bare setTimeout rather than in the caller's promise chain, so it would + // never reach that .catch - surface it through the same handler here. if (attempt < 60) { - setTimeout(() => this.initSetup(response, attempt + 1), 50) + setTimeout(() => { + try { + this.initSetup(response, attempt + 1) + } catch (err) { + this.handleGetdataFailure(err) + } + }, 50) } return } @@ -3890,7 +3908,8 @@ export class EditorComponent // The delete column is client-owned (see deleteRecordColumnRule.ts): // this asserts getdata did not send it, then prepends it to COLHEADERS so // it is the grid's first column. A throw here lands in the caller's - // .catch, which sets getdataError and shows the error state. + // .catch - or, on a retry, the branch above - which sets getdataError and + // shows the error state. prepareDeleteRecordColumn(response) this.cols = response.data.cols diff --git a/client/src/app/multi-dataset/multi-dataset.component.ts b/client/src/app/multi-dataset/multi-dataset.component.ts index b18d623..8166b80 100644 --- a/client/src/app/multi-dataset/multi-dataset.component.ts +++ b/client/src/app/multi-dataset/multi-dataset.component.ts @@ -368,13 +368,16 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit { // buildDatasetsObjects throws when a getdata response carries the // reserved delete-record column (see deleteRecordColumn.ts). This chain // has no .catch, so an uncaught throw here would become an unhandled - // rejection that aborts the loop with nothing shown. Log it and drop the - // parse, the way fetchDataset's own catch handles a failed getdata. + // rejection that aborts the loop with nothing shown. Surface it on + // screen, the way the editor flow refuses the same response, rather + // than leaving the user on an empty table selection with the file + // still attached. let datasetObjects: DatasetsObject[] = [] try { datasetObjects = this.buildDatasetsObjects(datasets) } catch (err) { console.warn('Error building dataset objects', err) + this.eventService.showInfoModal('Upload error', (err as Error).message) return }