withastro/astro · error · AstroError

LocalImageUsedWrongly

LocalImageUsedWrongly

Error message

`Image`'s and `getImage`'s `src` parameter must be an imported image or a URL, it cannot be a string filepath. Received `${imageFilePath}`.

What it means

In `verifyOptions()` (packages/astro/src/assets/services/service.ts:164), a string `src` that is neither remote nor starts with `/` — or starts with `/@fs/` — is treated as a filesystem path and rejected with `LocalImageUsedWrongly`. String paths bypass Astro's build-time image pipeline (no metadata, no hashing), so they are disallowed: local images must be ESM-imported so their dimensions and content are known.

Solutions

  1. Import the image and pass the metadata object: `import hero from '../assets/hero.png'` then `src={hero}` (dimensions come from the file).
  2. If the file lives in `public/`, reference it with a root-relative URL (`/hero.png`) — and note public images are served as-is, so `<img>` or explicit dimensions may fit better.
  3. For truly remote images, use the full URL starting with a protocol.

Example fix

// before
<Image src="/@fs/home/me/app/src/assets/hero.png" alt="Hero" width={800} height={600} />

// after
import hero from '../assets/hero.png';
<Image src={hero} alt="Hero" width={800} height={600} />
Defensive patterns

Strategy: type-guard

Validate before calling

const isLocalFilePath = (src: string): boolean =>
  src.startsWith('/@fs/') || (!src.startsWith('http') && !src.startsWith('/'));

if (typeof src === 'string' && isLocalFilePath(src)) {
  throw new Error(`Import the image instead of passing a filepath: ${src}`);
}

Type guard

function isImportableLocalPath(src: string): boolean {
  // usable as a string src only when public-root relative or remote
  return src.startsWith('http') || src.startsWith('/');
}
const needsEsmImport = (src: string): boolean => src.startsWith('/@fs/') || (!needsNoImport(src));
function needsNoImport(src: string): boolean {
  return src.startsWith('http') || src.startsWith('/') && !src.startsWith('/@fs/');
}

Prevention

When it happens

Trigger: `<Image src="../assets/hero.png" ...>`, `src="src/assets/hero.png"`, or `src="/@fs/home/me/app/src/assets/hero.png"` — any filesystem-ish string instead of an import or a remote URL.

Common situations: Porting plain `<img>` markup to `<Image>`; UI-framework components (React islands) that reference files by path; copy-pasting the `/@fs/` URLs visible in devtools during development.

Related errors


AI-assisted analysis of withastro/astro@e294953aa8 (2026-08-18). Data as JSON: /api/errors/8fd6d46e9be68dd6. Report an issue: GitHub.

Appendix: source

Thrown at packages/astro/src/assets/services/service.ts:175

	// `src` is missing or is `undefined`.
	if (!options.src || (!isRemoteImage(options.src) && !isESMImportedImage(options.src))) {
		throw new AstroError({
			...AstroErrorData.ExpectedImage,
			message: AstroErrorData.ExpectedImage.message(
				JSON.stringify(options.src),
				typeof options.src,
				JSON.stringify(options, (_, v) => (v === undefined ? null : v)),
			),
		});
	}

	if (!isESMImportedImage(options.src)) {
		// User passed an `/@fs/` path or a filesystem path instead of the full image.
		if (
			options.src.startsWith('/@fs/') ||
			(!isRemotePath(options.src) && !options.src.startsWith('/'))
		) {
			throw new AstroError({
				...AstroErrorData.LocalImageUsedWrongly,
				message: AstroErrorData.LocalImageUsedWrongly.message(options.src),
			});
		}

		// For remote images, width and height are explicitly required as we can't infer them from the file
		let missingDimension: 'width' | 'height' | 'both' | undefined;
		if (!options.width && !options.height) {
			missingDimension = 'both';
		} else if (!options.width && options.height) {
			missingDimension = 'width';
		} else if (options.width && !options.height) {
			missingDimension = 'height';
		}

		if (missingDimension) {
			throw new AstroError({
				...AstroErrorData.MissingImageDimension,

View on GitHub (pinned to e294953aa8)