withastro/astro · error · Error

App entrypoint must default export an object.

Error message

App entrypoint must default export an object.

What it means

Thrown by the dev toolbar plugin when an app is registered in object form (with id/name/entrypoint), the entrypoint is successfully imported, but its default export is not an object. The toolbar merges `{ ...app, ...loadedApp }`, so the loaded entrypoint must contribute an object (typically containing the app's component/hook callbacks).

Source

Thrown at packages/astro/src/toolbar/vite-plugin-dev-toolbar.ts:108

							try {
								let app;
								if (typeof appDefinition === 'string') {
									app = await importEntrypoint();

									if (typeof app !== 'object' || !app.id || !app.name) {
										throw new Error("Apps must default export an object with an id, and a name.");
									}
								} else {
									app = appDefinition;

									if (typeof app !== 'object' || !app.id || !app.name || !app.entrypoint) {
										throw new Error("Apps must be an object with an id, a name and an entrypoint.");
									}

									const loadedApp = await importEntrypoint();

									if (typeof loadedApp !== 'object') {
										throw new Error("App entrypoint must default export an object.");
									}

									app = { ...app, ...loadedApp };
								}

								return app;
							} catch (err) {
								console.error(\`Failed to load dev toolbar app from \${entrypoint}: \${err.message}\`);

								if (import.meta.hot) {
								import.meta.hot.send('astro:devtoolbar:error:load', { entrypoint: entrypoint, error: err.message })
								}

								return undefined;
							}

							return undefined;
						}

View on GitHub (pinned to d081033d5f)

Solutions

  1. Make the entrypoint default-export an object: `export default { id: 'myapp', name: 'My App', init() {}, ... }`.
  2. If you intend the default export to be a function/component, switch the registration to the string form so only id/name are validated.
  3. Verify there are no conditional or named-only exports masking the default export.

Example fix

// before — entrypoint.ts
export default function App() {}
// registration: { id, name, entrypoint: './entrypoint.ts' }

// after
export default { id: 'myapp', name: 'My App', init(canvas) {} };
Defensive patterns

Strategy: validation

Validate before calling

// In the entrypoint module's test:
import * as mod from './my-entrypoint.ts';
if (typeof mod.default !== 'object' || mod.default === null) {
  throw new Error('entrypoint must default-export a plain object');
}

Type guard

function isAppExport(val): val is { id: string; name: string; [k: string]: unknown } {
  return typeof val === 'object' && val !== null && typeof val.id === 'string' && typeof val.name === 'string';
}

Prevention

When it happens

Trigger: The entrypoint module does `export default function MyComponent() {...}` or `export default class {...}` or has no default export (undefined), then the object-form registration triggers `typeof loadedApp !== 'object'` after `importEntrypoint()`. A default export of a string, number, or function also triggers it.

Common situations: Authoring a toolbar app entrypoint that default-exports a function or component instead of an object. Forgetting the `export default` keyword entirely (loadedApp is undefined). Refactoring an entrypoint from object-export to function-export without updating the registration form.

Related errors


AI-assisted analysis of withastro/astro@d081033d5f (2026-08-12). Data as JSON: /api/errors/7e1b862bc0e2a506. Report an issue: GitHub.