Error handling: message rewrites and component fixes
Every user-facing error string in the platform, grouped by where it appears. Each row shows what ships today, what it becomes, and why. Rewrites follow the Neo standard: say what happened, why, and what to do next; one canonical phrasing per condition; the person is never the subject of the failure sentence; titles are noun-first so the failed thing leads. Component fixes appear inline on each affected row; fixes with no message row of their own are listed in Part 2.
84 messages · 62 rewrites shown below (61 rows; the two disconnect variants merge into one) · 22 unchanged or already fixed (omitted from this view)
Part 1 · Message rewrites and component fixes, by location
Global platform alerts 10 of 10 messages changing
Generic error alerts rendered anywhere a platform request fails.
Where
Before
After
Why
Service unavailable / 5xx
Alert graphql-error.component.tsx:116
Unavailable
Please wait while we attempt to reconnect.
Moderne platform unreachable.
The service is temporarily unavailable. We’ll keep trying — refresh in a moment to retry sooner.
Component fixMake the Retry / refetch affordance unconditional (today it renders only when the caller passes refetch)
Terse one-word title; no context; doesn’t name what’s unreachable.
500 / 400 network error
Alert graphql-error.component.tsx:121
Network error
Looks like we could not connect to our sources to find what you were looking for.
{section} failed to load.
The Moderne platform isn’t reachable right now. Check your connection and refresh.
Component fixMake the Retry / refetch affordance unconditional (today it renders only when the caller passes refetch)
No recovery step; chatty voice (“Looks like…”); retired verb form.
Title rule: noun-first, and callers pass {section} so the failed thing leads (“Recipe results failed to load.”). A generic “This content failed to load.” is the fallback only when no context exists.
No connected connector
Alert graphql-error.component.tsx:123
Trouble connecting to connector
We were unable to connect to or find a connector required to display this content.
{feature} failed to load.
A required connector isn’t connected. Ask a Moderne admin to enable the required connector.
Component fixMake the Retry / refetch affordance unconditional (today it renders only when the caller passes refetch)
Doesn’t name the connector or the feature; no next step.
GraphQL fallback
Alert (fallback) graphql-error.component.tsx:125
GraphQL error
This page failed to load.
The Moderne platform returned an unexpected response. Refresh, or contact support if it persists.
Component fixMake the Retry / refetch affordance unconditional (today it renders only when the caller passes refetch)
Names Moderne’s internal transport, not the user’s problem. Even for a developer audience there’s nothing actionable in “GraphQL error”; no why; no next step.
Unauthorized action
Alert graphql-error.component.tsx:110
Unauthorized
You are not authorized to perform this action.
{action} requires permission.
Your account doesn’t have permission. Contact your organization admin to request access.
Component fixMake the Retry / refetch affordance unconditional (today it renders only when the caller passes refetch)
No why, no next step, and the person is the subject of the failure sentence. Path A reframes to the account.
Title rule: permission errors title as rule statements; callers pass the attempted action (“Deleting this organization requires permission.”). A generic “This action requires permission.” is the fallback only when the action is unknown.
SCM token exchange failed
Alert graphql-error.component.tsx:108
Unable to authorize with your SCM provider.
Authorization with {provider} failed.
The access token may have expired or been revoked. Reconnect your source control provider in Settings → SCM connections.
Doesn’t name the provider; no next step.
Not found (GraphQL)
Alert graphql-error.component.tsx:114
Not found
Unable to find the requested resource.
{resource type} not found: {id}.
It may have been deleted or moved. Return to the previous page or check the URL.
Component fixMake the Retry / refetch affordance unconditional (today it renders only when the caller passes refetch)
“Requested resource” is vague; no why; no next step.
Data integrity
Alert graphql-error.component.tsx:112
Unable to find the data you requested
We encountered an issue with retrieving your request.
{section} failed to load.
The Moderne platform returned incomplete data. Refresh, or contact support if it persists.
Component fixMake the Retry / refetch affordance unconditional (today it renders only when the caller passes refetch)
Title and body describe two different problems; no next step.
Same header rule: interpolate {section} from the caller; generic fallback only when unknown.
Unknown error (fallback body)
Alert graphql-error.component.tsx:102
An unknown error has occurred.
Request failed.
The cause is unknown. Try again, or contact support if it persists.
Component fixMake the Retry / refetch affordance unconditional (today it renders only when the caller passes refetch)
Third phrasing of the unknown-error fallback shipped across the app. All collapse to the titled catch-all.
Connector tools not configured
Empty state connector-components/no-connected-connector.component.tsx:31
Looks like you have not configured any tools for your connector…
No tools configured for this connector.
Configure tools for the connector, then refresh.
Another chatty “Looks like…” opener. Noun-first, what + next step.
Error pages 3 of 3 messages changing
404, 500, and the auth error boundary.
Where
Before
After
Why
500 page
500 page error-500.component.tsx:61/70
Full page
We are experiencing a technical issue.
Contact Support if the problem persists.
Full page
This page failed to load.
The Moderne platform encountered an unexpected error. Refresh, or contact support if it persists.
Structurally compliant, but the formal voice is off-platform. Rewrite adopted: one voice on the highest-stakes surface.
Auth boundary
Auth error page pages/auth/error.tsx:69
Full page
Well, that was unexpected.
Please try to sign in again
Full page
Sign-in failed.
Authentication didn’t complete successfully. Sign in again.
Component fixAdd role="alert" to the page message container
Chatty voice clashes with the platform’s neutral tone. Also add role="alert" (live-verification finding).
Page 404
404 page pages/404.tsx
Full page
The page you’re looking for was not found.
It may have been moved, deleted, or the URL may be incorrect. Contact support if you think this is a mistake.
Full page
The page you’re looking for was not found.
It may have been moved, deleted, or the URL may be incorrect. Contact support if you think this is a mistake.
Component fixAdd role="alert" to the message container. Copy unchanged — compliant three-part structure.
The words are already right; the remaining work is component-level (live verification found no ARIA role on the message container).
The organization may have been deleted or your access was removed. Switch organizations from the selector.
Bare title; no why; no next step.
The string lives in the helper constant, not the selector — change it there, or the rewrite lands wherever the constant is inlined.
Load repositories from organizations
Toast manage-user-organizations.component.tsx:127
Toast · bottom-right
✕
Failed to load repositories from selected organizations: {error}
Toast · bottom-right
✕
Repositories failed to load from {orgCount} organization(s).
{error}. Try again.
Retry
Component fixToast persists until dismissed + gains a Retry action (one provider fix — see Component-only fixes)
Verb drift; no next step. The raw error stays as diagnostic detail, after the canonical sentence.
Import organizations
Toast manage-user-organizations.component.tsx:169
Toast · bottom-right
✕
Failed to import {count} user-configured organization(s)
Toast · bottom-right
✕
Import failed for {count} organization(s).
The Moderne platform rejected the request. Try again.
Retry
Component fixGains a Retry action via the provider fix (persistence is already correct here: duration null)
Verb drift; no why; no next step.
Copy to clipboard
Toast organizations.table.component.tsx:456
Toast · bottom-right
✕
Failed to copy to clipboard
Toast · bottom-right
✕
Copy failed.
The browser blocked clipboard access. Check permissions and try again.
Retry
Component fixToast persists until dismissed + gains a Retry action (one provider fix — see Component-only fixes)
One of two verbs shipped for the same clipboard failure.
Tree node load failure
Inline tree node organizations.table.component.tsx:294 · org-filtered-tree.component.tsx:516/689
Tree node
▸ openrewrite · Failed to load — click to retry
Tree node
▸ openrewrite · Unable to load — click to retry
Recovery lives inside the error text. Adopted as canonical for load failures; opener aligned to the palette.
No repositories selected
Form warning (top of form) user-organization-editor.component.tsx:268
Please select at least one repository before saving
At least one repository is required. Select at least one repository, then save.
Component fixMove the warning inline onto the repository grid
Shipped copy is “…before saving”, at line 268. Composite validation. Copy becomes a rule statement and moves inline on the repository grid rather than sitting at the top of the form.
Chat / AI assistant 6 of 16 messages changing
Everything the assistant says when a request fails.
Where
Before
After
Why
LLM unreachable
Chat alert error-chat-reply.component.tsx:35
Chat reply
Could not reach the model service provider. Please check network connectivity and try again.
Chat reply
Unable to reach the model service provider. The service isn’t responding. Check your network and try again.
Verb drift only, aligned to the canonical opener.
Turn cancelled
Chat alert error-chat-reply.component.tsx:45
Chat reply
Conversation turn was cancelled.
Chat reply
Conversation turn was cancelled.
Component fixRe-skin to severity="info" (role="status")
The copy is fine, but a user-initiated cancellation is information, not an error. Re-skin to info severity (role="status").
Model request failed
Chat alert error-chat-reply.component.tsx:38
Chat reply
I’m sorry, but something went wrong with the model request.
Chat reply
Unable to complete the model request. The model service returned an error. Try rephrasing your request.
Apologetic voice; no why; no next step, on a surface where users are most stuck.
Internal error
Chat alert error-chat-reply.component.tsx:46
Chat reply
Something went wrong on our end. Please try again in a moment.
Chat reply
Unable to complete the request. The Moderne platform hit an internal error. Try again in a moment.
Generic and non-actionable as it ships. The platform knows this failure is internal, so the message can say so. Distinct condition from the unknown-cause fallback below.
I’m processing your request. Please hold on for a moment.
Loading state
Processing your request…
Component fixMove out of ErrorChatReply into a loading component
Not an error at all, yet it renders inside the error-reply component. Live verification: already severity="warning" visually; remaining work is semantic (move to a loading state).
Recipe run results 7 of 7 messages changing
Status badges and their popover bodies on the run results screen. Badges render via NeoBadge; popover content renders via DataGrid/ExtendedErrorCard (not a raw inline alert).
After the ExtendedErrorCard → ErrorDetailPre swap (Part 2), clicking a status badge shows the full stack trace in a scrollable code block — which is why several rewrites below end with "Open the status badge for the stack trace."
Where
Before
After
Why
FAILED_LOAD_AST
Status badge repository-results.helpers.ts:13
Repository results · status column
spring-petclinicLST loading failed
Repository results · status column
spring-petclinicLST loading failed
LST loading failed for {repo}. The repository’s LST could not be parsed. Check LST status on the Repositories page. If the LST is missing, an admin needs to re-ingest the repository.
Badge stays terse; it gains a paired full-sentence body that names the repository.
FAILED_LOAD_RECIPE
Status badge repository-results.helpers.ts:14
Repository results · status column
spring-petclinicRecipe loading failed
Repository results · status column
spring-petclinicRecipe loading failed
Recipe loading failed for {repo}. The recipe could not be loaded into the runner. Re-launch the recipe from its recipe page.
Same treatment: terse fragment + paired body.
RECIPE_ERROR
Status badge repository-results.helpers.ts:15
Repository results · status column
spring-petclinicRecipe error
Repository results · status column
spring-petclinicRecipe failed
Recipe failed on spring-petclinic
The recipe threw an exception during execution. Re-launch from the recipe page, or contact the recipe author if it persists.
java.lang.NullPointerException: Cannot invoke "String.length()" because "s" is null
at com.example.MigrateSpringBoot.visit(MigrateSpringBoot.java:47)
at org.openrewrite.java.JavaVisitor.visitClassDeclaration(JavaVisitor.java:118)
at org.openrewrite.java.tree.J$ClassDeclaration.acceptJava(J.java:1247)
at org.openrewrite.java.tree.J$ClassDeclaration.accept(J.java:1231)
at org.openrewrite.Recipe.lambda$visit$0(Recipe.java:302)
... 47 more
Caused by: java.io.IOException: LST cache missing for spring-petclinic
at org.openrewrite.LstCache.load(LstCache.java:88)
at org.openrewrite.LstCache.get(LstCache.java:62)
... 12 more
Two words, no detail anywhere. Canonical string: “Recipe failed on {repo}”.
TIMEOUT
Status badge repository-results.helpers.ts:16
Repository results · status column
spring-petclinicTimeout
Repository results · status column
spring-petclinicTimeout
Recipe timed out on {repo}. The run exceeded the per-repo time limit. Re-launch the recipe from its recipe page, or contact support if it persists.
A single word carrying the whole failure.
NO_LST (skipped)
Status badge repository-results.columns.tsx:78
Repository results · status column
spring-petclinicRepository has no indexed LST and was skipped
Repository results · status column
spring-petclinicSkipped — no LST
{repo} was skipped — no LST available. Ingest an LST for this repository, then re-launch the recipe.
An error occurred while running this recipe. Please try running the recipe again.
migrate-spring-boot-3-2 failed on spring-petclinic
The run failed during execution. Re-launch the recipe from its recipe page to retry.
[ERROR] Recipe execution failed for spring-petclinic
[ERROR] org.openrewrite.RecipeException: NullPointerException in MigrateSpringBoot
[ERROR] at org.openrewrite.Recipe.visit(Recipe.java:302)
[ERROR] at moderne.cli.RunCommand.execute(RunCommand.java:184)
[ERROR] at moderne.cli.Main.main(Main.java:57)
[ERROR] Caused by: java.io.IOException: LST cache missing
[ERROR] at org.openrewrite.LstCache.load(LstCache.java:88)
[ERROR] Exit code: 1
Component fix (pending audit)Confirm CliErrorOutput renders with the same code-block chrome as the ExtendedErrorCard fix (neutral-light tint, 1px border, white-space: pre, max-height: 40vh scroll). If it doesn't, apply the same treatment.
“An error occurred” is a retired opener; doesn’t name the recipe or repo.
The string actually lives in cli-error-output.component.tsx:12.
Apply filter
Alert results-summary.component.tsx:548
Couldn’t apply filter
Showing the last loaded results — try the filter again.
Filter failed to apply
Showing the last loaded results — try the filter again.
Verb drift only; structure and recovery already compliant.
Commit dialog 5 of 6 messages changing
Warnings before committing changes.
Where
Before
After
Why
Results still in progress
Dialog info mass-refactor.component.tsx:177
Results are still in progress
Results are still in progress for {count} repositories.
You can commit now or wait for all results.
Could name how many, and what the user’s options are.
Commits unavailable
Button tooltip mass-refactor.component.tsx:114
Button tooltipCommits are unavailable
Button tooltipCommits are unavailable. No repositories have successful results yet.
Doesn’t say why. Is it missing SCM auth, or no successful results?
Resolved: two enumerated reasons from the code paths. No successful results: “No repositories have successful results yet.” Missing SCM auth: “Authorize with {provider} to enable commits.” The tooltip states whichever applies.
Destination organization not found
Field helper (hand-rolled) organization-option.component.tsx:71
Commit dialog · destination field
acme-orq
This organization does not exist
Commit dialog · destination field
acme-orq
Organization not found. Check the name, or choose a different destination.
Component fixFormHelperText + TextField error instead of the hand-rolled red div
Hand-rolled red div becomes a proper field error (FormHelperText + error state); copy gains a next step and a noun-first opener.
Access requirements notice
Helper text organization-option.component.tsx:83
Bitbucket only: You must have read access on the source repository and admin access on the destination organization of the fork.
For Bitbucket, committing requires read access on the source repository and admin access on the destination organization of the fork. Contact your organization admin to request access.
Path A: the person was the subject (“You must have…”). The rule statement keeps the same information without the blame reading, and adds who to ask.
The shipped string is scoped “Bitbucket only” and mentions the fork; the rewrite keeps the provider qualifier so GitHub / GitLab users aren’t misled.
GitHub access notice
Helper text organization-option.component.tsx:80
GitHub only: Moderne requires access to this organization.
For GitHub, Moderne requires access to this organization. Ask your organization admin to grant Moderne access.
Sibling rule to the Bitbucket notice. Same rule-statement treatment, provider scope preserved.
File diff view 2 of 6 messages changing
Per-file errors and notices in results diffs.
Where
Before
After
Why
File-level run error
Alert file-diff-view-results.components.tsx:182
Recipe execution error
Recipe failed on {file}.
The recipe could not be applied to this file. Open the status badge for the stack trace, then re-launch the recipe from its recipe page.
Bare title; no why; no next step.
Can’t retrieve changes
Alert file-diff-view-results.components.tsx:159
Unable to retrieve changes.
There may have been a problem with parsing the source code, retrieving the changes, or running the recipe on this file.
Changes unavailable for {file}.
The source could not be parsed or the recipe failed on this file. Re-launch the recipe from its recipe page.
Hedged why (“may have been a problem with… or… or…”); no next step.
Recipe pages & dialogs 2 of 2 messages changing
Recipe details and the import-recipes dialog.
Where
Before
After
Why
Recipe details failed to load
Toast recipe-details page
Toast · bottom-right
✕
Failed to load recipe details
Toast · bottom-right
✕
Recipe details failed to load.
The Moderne platform isn’t responding. Refresh to try again.
Retry
Component fixToast persists until dismissed + gains a Retry action (one provider fix — see Component-only fixes)
No why, no next step; opener aligned to the canonical form.
Surface check: per the Error Handling decision matrix, a page section that fails to load is an inline Alert (a durable state), not a transient toast. Flagged for the implementation pass.
Access clipboard
Toast load-external-recipes-dialog.component.tsx
Toast · bottom-right
✕
Couldn’t access the clipboard.
Toast · bottom-right
✕
Clipboard unavailable.
The browser blocked clipboard access. Check permissions and try again.
Retry
Component fixToast persists until dismissed + gains a Retry action (one provider fix — see Component-only fixes)
The other verb. Both align to one canonical clipboard string.
Builder 5 of 5 messages changing
Recipe builder pages and dialogs. Three precondition toasts move to inline validation; two unknown-error fallbacks align to the titled catch-all.
Component fixRemove the toast call; render as an inline alert with role="alert"
The shipped “Please save it first.” holds — the condition is user-fixable (saving assigns the ID). Inline alert keeps the what + next step; noun-first title.
Builder page fallback
Alert pages/builder/[recipeRunId].tsx:137
An unknown error occurred.
Request failed.
The cause is unknown. Try again, or contact support if it persists.
Hand-rolled fallback; aligns to the titled catch-all from the canonical catalog.
The cause is unknown. Try again, or contact support if it persists.
Same fallback, another hand-rolled copy. One canonical string replaces all of them.
Visualizations 5 of 5 messages changing
Running and loading visualizations.
Where
Before
After
Why
Visualization not found
Alert visualization-run.component.tsx:37
Unable to find visualization: {id}
Visualization not found: {id}.
It may have been deleted. Return to the previous page.
Names the id (good) but gives no why and no next step.
Start visualization run
Toast visualization-card.component.tsx:136
Toast · bottom-right
✕
Unable to start visualization run.
Toast · bottom-right
✕
Visualization run failed to start for {viz}.
The platform rejected the request. Try again.
Retry
Component fixToast persists until dismissed + gains a Retry action (one provider fix — see Component-only fixes)
No why; no next step; doesn’t name the visualization.
Run visualization (with detail)
Toast visualization-card.component.tsx:144
Toast · bottom-right
✕
Error attempting to run visualization: {error.message} Please try again.
Toast · bottom-right
✕
Visualization {viz} failed to run.
{error.message}. Try again.
Retry
Component fixToast persists until dismissed + gains a Retry action (one provider fix — see Component-only fixes)
“Error attempting to” is a retired opener, and the raw error is the whole title. The detail is useful to developers, so it stays; it moves into the body, after the canonical sentence.
The MCP server isn’t responding. Click Retry, or refresh the page.
Same treatment.
Read resource
Error state resource-viewer.component.tsx:40
Failed to read resource
{error.message}
Resource {name} failed to load.
{error.message}. Try again.
Verb drift (load vs read), and the raw error is the only body. The detail stays, preceded by a sentence saying what actually failed.
Get prompt
Error state prompt-list.component.tsx:60
Failed to get prompt
{error.message}
Prompt {name} failed to load.
{error.message}. Try again.
Verb drift (load vs get); same treatment: canonical sentence first, raw detail after.
Settings 1 of 1 message changing
Where
Before
After
Why
Update profiling
Toast profiling-settings.component.tsx:27
Toast · bottom-right
✕
Failed to update system profiling — please try again.
Toast · bottom-right
✕
System profiling update failed.
The Moderne platform rejected the change. Try again.
Retry
Component fixToast persists until dismissed + gains a Retry action (one provider fix — see Component-only fixes)
Verb drift only.
Part 2 · Component-only fixes
Component fixes that have no message row of their own. The first two are systemic: fixing them once delivers the component fixes noted on the rows above.
When to use ErrorDetailPreFor any error content that carries code, stack traces, CLI output, or protocol payloads, use ErrorDetailPre — the tinted, bordered, scrollable code block already shipping in VisualizationErrorAlert, RepositoryErrorAlert, and CliErrorOutput. Curated human sentences stay in the alert body; only raw payloads move into ErrorDetailPre. The two rows below (ExtendedErrorCard, render-mermaid) are one-line swaps to consolidate on this pattern.
Where
Finding
Fix
helpers/logger.helper.ts:1
Blocker. logError() is an empty stub. Every boundary calls it; the spec requires tenant + URL + stack logged on catch. Nothing is recorded.
Implement logError() with tenant, URL, message, and stack. Fix first; everything downstream depends on it.
contexts/notification.context.tsx
Systemic. Error toasts auto-dismiss after 6 seconds, there is no Retry / action API, and nothing deduplicates. Every toast row above inherits all three.
Force autoHideDuration: null when severity is error; add an optional action field to NotificationOptions; deduplicate with a count. One provider change fixes every toast.
Ad-hoc try/catch without delegating to a boundary or an inline alert.
Evaluate per case: surface via boundary or inline Alert.
extended-error-card.component.tsx
In production: live via ErrorStatusPopover (error-status-popover.component.tsx:8, 34), which renders in repository-results.columns and recipe-artifacts.columns.
Keep the pattern in the docs — it is live.
extended-error-card.component.tsx:83–93
Message body wraps in PreformattedWithBreak — monospace, but no tint, no border, no scroll, no max-height; pre-wrap breaks stack frames mid-classpath. A 25-line trace (see the LongStackTrace story) pushes the popover off-screen.
One-line swap: replace PreformattedWithBreak with ErrorDetailPre — the app's canonical error-content code block, already used by VisualizationErrorAlert and RepositoryErrorAlert. Delivers neutral-light tint, border, white-space: pre, and max-height: 40vh scroll. CopyPasta header is untouched.
Two bare <pre> — one for the parse error, one for the source diagram. No tint, no border, no scroll, no max-height. Multi-line Mermaid source overflows the alert.
Wrap both in ErrorDetailPre so the source doesn't spill out of the alert. Same one-line swap.
role="alert" unverified on these custom surfaces (they don’t inherit MUI’s severity-to-role mapping).
Verify each announces; set the role explicitly where missing.
constants/messages.ts
Canonical strings are scattered: ERROR_COPY_BY_CODE (chat) and ModerneGraphQLError each hold their own catalog.
Fold into the single catalog; a catalog entry becomes definition-of-done for new error UI.
Prioritized fix list
Implement logError() to log tenant + URL + stack (helpers/logger.helper.ts:1). Everything downstream depends on it.
Force autoHideDuration: null for error toasts in notification.context.tsx, and add an optional action field to NotificationOptions for Retry buttons.
Move precondition checks out of toasts in use-install-and-run-builder-recipe.hooks.ts:86/91/98; surface as form / inline validation. Copy drafted (Part 1 · Builder).
Replace the two hand-rolled red-text form errors (organization-option.component.tsx:71, text-editor-input.component.tsx:73).
Delete the duplicate PageErrorBoundary at pages/_app.tsx:238; add logError() to markdown.error-boundary.tsx:12.
Sweep the six generic “unknown error / something went wrong” strings — all six rewrites are drafted in Part 1 and align to the titled catch-all (“Request failed. The cause is unknown. Try again, or contact support if it persists.”) or its chat sentence form.
Ship the ExtendedErrorCard code-snippet fix so long stack traces render as a scrollable code block (neutral-light tint, border, white-space: pre, max-height: 40vh) instead of pushing the popover off-screen. Standardize empty states on EmptyState / CenteredEmptyState (visualization-run, mcp-explorer).