Skip to content

fix: show real API errors and stop stuck loading states - #316

Merged
MrYuion merged 9 commits into
developfrom
fix/async-error-handling
Sep 30, 2026
Merged

MrYuion merged 9 commits into
developfrom
fix/async-error-handling

Conversation

@MrYuion

@MrYuion MrYuion commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

Error handling was inconsistent across the app:

  • ts-client throws the raw Response. Most toasts printed {}, [object Response], escaped JSON or a whole HTML error page.
  • Many handlers caught the error, then showed the success toast anyway.
  • Some handlers showed a toast and then rethrew into a template, which gave unhandled rejections. Others had no catch, so spinners and modals stayed open for good.
  • Loaders shared one _loading signal, so the first to finish hid the bar.
  • waitForSignalValue and upload polling had no limit. AsyncHandler lost the handle of a timer that set itself again.
  • There were two loaders for the current user.

This is part of a stack of 6 PRs from a review of the whole app. Merge them in order: confirm-cancel, security, data loss, broken features, async errors, cleanup. Each PR targets the branch before it, so the diff shows only its own changes.

Fix

  • common/errors.ts has describeError and readError. readError reads the JSON message or error, short text, or falls back to the status. All error toasts use one of them.
  • Success toasts show only on success. The bulk driver update reports how many failed.
  • Handlers notify and return. Loaders and deletes close their modal or reset loading in finally.
  • Loading state comes from each resource's isLoading().
  • waitForSignalValue rejects after 60 s. Guards send the user to the unauthorised page. Upload polling has a limit.
  • current_user and BackofficeUsersService.user are one signal.
  • Plain fields that changed after an await are now signals.

Checks

  • tsc, bun run lint, bunx vitest run (1034 tests) pass. New tests cover readError, the timer fix and the wait limit.
  • All 6 branches were merged and tested in a browser against a local PlaceOS stack (nightly images, with core and triggers). Test agents created their own records, checked results through the API, and removed the records after. Save failures on system, zone, trigger, domain, application, group, user, repository and driver forms show messages like "500 Internal Server Error: qa error", for JSON, HTML and empty bodies.

Made by Claude Opus 5.5 (1M context) in Claude Code (T3 Code).

🤖 Generated with Claude Code

MrYuion and others added 9 commits September 30, 2026 13:13
ts-client rejects with the raw Response on a non-OK status. The ad-hoc
`JSON.stringify(err.response || err.message || err)` formatting showed
"{}" or "[object Response]" instead of the reason.

Move describeError to common/errors.ts and use it at every site. Add
readError, which also reads the body of an unread Response, and use it
where the API body carries the useful message (settings, bulk and form
saves).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Several actions caught the error, showed an error toast, then carried on
to the success toast. Return early on failure instead:

- recompileDriver
- bulk driver update (now uses allSettled and reports the failed count)
- joinModule, removeModule, addZones, removeZone on systems
- system zones keep pending zones when the save fails

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Return after notifyError instead of rethrowing into template handlers
  (groups, users, zones, api keys, Azure integration)
- Catch trigger selection errors and always close the select modal
- Catch list loader errors and always clear the loading state (admin
  interfaces, edge, brokers, schemas, cluster, platform details,
  metadata history)
- Validate schema JSON before save and catch save errors
- Close the confirm modal and report the error on failed deletes
  (storage, uploads, api keys)
- Read api key resources with hasValue(), as value() throws in the
  error state
- Skip zones the user cannot load instead of failing the whole list
- Use allSettled for auth sources so one failing type does not hide all
- Report group and user membership load errors instead of showing an
  empty list
- Authenticated images: check response.ok, ignore stale results, log
  failures, and bound the wait for the API authority
- uploadFileWithPermissions always settles
- Catch service worker update check failures

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Several loaders toggled one shared `_loading` signal, so the first loader
to finish hid the progress bar while others still ran. Loaders that
returned a promise without awaiting it cleared the flag at once.

Derive `loading` from the resources' isLoading() in the group, user,
zone, driver and trigger state services. Bulk adds keep their own flag.

The system module loader now ignores results from an aborted load or a
system that is no longer active, so it cannot overwrite the current
module list or clear its loading flag.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- AsyncHandler.timeout clears the stored handle before it calls the
  callback, so a callback that reschedules itself under the same name
  keeps its new handle. The name and callback error messages were
  swapped.
- waitForSignalValue rejects after a max wait (60s by default). Guards,
  settings, item service and app init handle the timeout.
- App init uses the bounded wait in place of its own 30s user timer, and
  catches staff tenant check failures.
- Image list uploads start the status poll only after an upload starts,
  and a cancelled permissions modal no longer leaks the interval.
- Remove the second current user loader in user-state.ts. It could leave
  current_user null forever. BackofficeUsersService now publishes the
  user there, so current_user and BackofficeUsersService.user are the
  same signal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With zoneless change detection, a plain field set after an await does
not re-render.

- Trigger comparison form: status variable lists are signals, and a
  failed module query shows an error.
- System trigger form: loading is a signal, and it resets when saving
  the trigger settings fails. The failure no longer rethrows into the
  template handler.
- Object list field: emit new objects and arrays instead of mutating
  the parent's objects through ngModel. The delete button is
  type="button" so it cannot submit the parent form.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Entity form saves use readError instead of JSON.stringify on the
  response body, so toasts show the API message, short text, or the
  status instead of escaped JSON, HTML pages, or "{}"
- Domain settings save, extension save, zone tree import/export and
  signage AI provider removal report the error body too
- Tenant delete says "tenant", and a successful delete now shows the
  success toast and reloads the list
- Test readError with a JSON error field and an empty body

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Resource imports: skip the system lookup when no resource has an
  email, and catch lookup and import failures so loading resets
- Upload library: drop cancelled uploads before polling, and show an
  error when the uploads list fails to load
- Zone about: catch parent zone load failures
- Zone and system trigger removal: notify and return instead of
  rethrowing

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Metadata delete, execute method, module power toggle, signage AI
  provider save and storage provider save no longer rethrow into
  template handlers, so no unhandled rejection reaches the console
- Signage AI provider and storage provider save toasts include the
  API error detail

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@vercel

vercel Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
backoffice Ready Ready Preview Sep 30, 2026 3:25am UTC

Base automatically changed from fix/broken-features to develop September 30, 2026 04:56
@MrYuion
MrYuion merged commit fcee248 into develop Sep 30, 2026
3 of 5 checks passed
@MrYuion
MrYuion deleted the fix/async-error-handling branch September 30, 2026 04:57

This branch was successfully deployed

1 active deployment
Preview — 4864b155 Deployed Sep 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant