I expected the error to fire every time I refresh the page however it only appears to fire it the first time. I do a hard refresh to ensure it’s not just caching but still doesn’t fire. Only if I change the src to a different URL will it fire again (only once).
If I do the same code in a fresh React app it works as expected, or if I use next/image it works and alerts every time:
@harrjm This is actually a bit of a complicated issue, but I’ll try my best to explain what’s happening.
First, understand that adding "use client" to a component doesn’t automatically stop all SSR logic from running on the component.
Even with "use client", adding a callback like onError to an element in React won’t actually render an element with onerror in the DOM. Instead, React will set up a listener for errors using JavaScript and run your callback when the JS listener catches an error. This is partly because JS Functions are not serializable and Next.js still uses SSR at its core (layout.tsx and the main server).
What’s happening in your case is this:
The DOM loads, and an <img src="/does-not-exist.jpg" /> is already on the page (instead of waiting for React to load, Next.js sends the HTML first, React mounts and “hydrates” later)
The image URL is a 404, so the image throws an error
This happens early in the page load, and React has not yet set up its JavaScript listener for errors, so nothing handles this error
As far as why it sometimes works, I’ll try to explain below:
It fires the first time you visit the page, likely because the request to /public/does-not-exist.jpg takes longer than normal the first time it’s called on the next dev server.
When you change the src while using the dev server, the React component is already hydrated and fully controlled by React on the client. The onError callback in JS is already set up as well. So when the src changes, a new request will be made, this time causing an error that actually get handled.
Everything works fine in a “fresh React app” because the initial HTML is basically just <div id="root"></div> and React is responsible for rendering the DOM inside that element, so it attaches the onError callback before any error occurs.
Since the <Image> component from next/image is a React component itself, so it mounts and handles events more properly in accordance with React.
The best thing you can do to solve all this if you still want to use the <img> tag is to ensure that the src is not set until the component is mounted client side. Here’s a raw implementation, you could extract this to a nice reusable component if you like:
"use client";
import { useEffect, useState } from "react";
const MyImageComponent = () => {
const [imageSrc, setImageSrc] = useState<string | null>(null);
const url = "/does-not-exist.jpg";
// Effects only run on the client-side after the component has mounted
useEffect(() => {
setImageSrc(url);
}, [url]);
// You could also show a placeholder here
if (!imageSrc) {
return null;
}
return (
<img
src={imageSrc}
onError={() => {
alert("Client-side error fired");
}}
alt="A test image"
/>
);
};
export default MyImageComponent;