Your "no camera found" is not about a camera
The dialog says no camera was found. The camera is right there, and the user is looking into it. The error is honest — it is just answering a different question from the one the message asks, and the call that would disambiguate it was made too early to help.
On this page
TL;DR: A getUserMedia NotFoundError is raised when there is no camera — and also when the operating system is refusing your browser access to one. The name describes the first case and the message your application renders describes the first case, so a user with a working camera is told to go and buy a camera. The call that separates the two is
enumerateDevices(), and the systems that get this wrong are usually calling it already: just before, where it cannot help, instead of after, where it can.
Support tickets for this are unmistakable once you have seen one. A user says the app cannot find their camera. Attached is a photograph, taken on a phone, of a laptop with its camera indicator plainly visible. Sometimes they have already bought a second webcam.
The application is not lying to them. It is repeating, faithfully, a name the platform gave it.
Where does NotFoundError actually come from?
There are at least two roads to it and the API does not distinguish them.
The first is the obvious one: there is no camera. No hardware, no driver, nothing to open.
The second is a system-level denial. On macOS, where the operating system keeps its own
per-application camera control, a browser that has been refused there has no device to offer you, and
it has to reject with something. One browser resolved a bug about exactly this —
silent failure when the user had previously refused at the OS level
— by making the call return NotFoundError. That was a deliberate improvement on failing silently,
and it also permanently fused two different causes into one name.
This is not folklore, and it is on the record twice — from two editors, five weeks apart, in a thread
about something else. One
raised it as an aside:
"for getUserMedia, NotFoundError is ambiguous between no device and OS blocking access" — Youenn
Fablet, January 2024. The other
answered it:
"Apps can disambiguate NotFoundError using enumerateDevices(). But yes gUM here is a side-track." —
Jan-Ivar Bruaroey, February 2024, also
mirrored to the working group's list.
The second half of that sentence is why this is still your problem. The thread was about screen capture, and the resolution it produced is about screen capture. The camera case was acknowledged, handed a one-line remedy, and left exactly where it was: in application code. Nobody is coming to disambiguate the error for you.
- Ask what devices existbefore requesting permission
- One entry per kind comes backright kind, no identity
- The presence check passesa camera is in the list
- getUserMedia returns the real errorthe check decided nothing
Why doesn't checking the device list first solve it?
This is the intuitive fix, both systems we read implement it, and it does not work. It is worth being precise about why, because the reasoning that produces it is sound and the conclusion is wrong.
The idea: before asking for a camera, ask what devices exist. If the list is empty, say so; if it has
a camera, ask for permission. Both systems do this, in different stack generations, and both throw
their own hand-built NotFoundError from the check:
navigator.mediaDevices.enumerateDevices().then((info) => {
if (info.length === 0) throw notFound(NO_CAM_AND_MIC);
if (!hasKind(info, 'videoinput')) throw notFound(NO_CAM);
if (!hasKind(info, 'audioinput')) throw notFound(NO_MIC);
return navigator.mediaDevices.getUserMedia(constraints);
});The specification requires truncation, not omission — so a camera that was discovered survives the
absence of permission. Where camera information may not be exposed, the enumeration algorithm
says
"truncate cameraList to its first item",
and the entry that survives is built by
a step that returns early
— "If deviceInfo.kind is equal to "videoinput" and camera information can be exposed on
mediaDevices is false, return deviceInfo" — returning before label, deviceId and groupId are
ever initialised. So you get one entry per kind, carrying a valid kind and three empty strings.
Truncating a list of three cameras leaves one. Truncating a list of none leaves none. A device that exists does not vanish from the list because you have not been allowed to open it — and that is a requirement on the implementation, not a quirk of one.
It is also what two engines do on the OS in question, said by the person answering that exact
question upstream. Asked what enumerateDevices() returns when the OS is blocking, Bruaroey's reply
was "This is known. Both Chrome and Firefox report 1 camera and 1 microphone without macOS
permission."
And we ran it. One engine, headless, on a machine with three microphones: before permission it
reports one camera, one microphone and one speaker, every label, deviceId and groupId blank.
Grant permission and the same call reports three microphones. That difference is the truncation,
happening in front of the harness — and the camera side cannot show it, because the machine has one
camera either way. We also ran it inside a cross-origin frame that is not allowed to use the camera
at all, on the theory that the algorithm's per-device sub-steps abort there and the list would come
back empty: it does not. The frame reports the same three blank entries, so a blocked frame and an
un-permissioned page are indistinguishable by the list alone.
So on a machine with a camera, this check passes and falls through to getUserMedia, which returns
the correct error for whatever is actually wrong. The gate is harmless. It is also useless, and
it is useless in a specific way that matters: it consumes the one call that could have answered the
question, at the one moment when the answer is not yet available.
- Microphones listed, permission granted3
- Microphones listed, before permission1
- Cameras listed, permission granted1
- Cameras listed, before permission1
When does the same call become useful?
After the failure, not before.
enumerateDevices() before you ask for permission tells you what the platform is willing to admit to
without permission — which, by design, is the shape of your hardware and none of its identity. Run
the same call after getUserMedia has rejected with NotFoundError and the question changes from
"what exists" to "what does the platform say exists now that it has already refused me". If a
videoinput is present in the list and the request still failed with a not-found name, the hardware
is not the problem.
That is the ordering Bruaroey's one-line answer points at. Two signals, four readings, and only one of them is about hardware:
getUserMedia result |
Camera in the list afterwards | What is actually true | What to say |
|---|---|---|---|
NotFoundError |
yes | the OS or the platform is refusing access | "We could not access your camera — check your system privacy settings" |
NotFoundError |
no | no camera is visible to the browser | "No camera detected" |
| resolves | — | the request succeeded — whether the track carries pictures is a separate question | — |
The rest of the remedy is ordering:
- Ask first, enumerate second. Let
getUserMediafail, then interrogate. - Branch on the pair, not on the name.
NotFoundErrorplus a camera in the list is a blocked camera.NotFoundErrorplus no camera in the list is a missing camera. Those deserve different sentences, and only one of them should ever mention buying anything. - Enumerate while the page is still visible. The algorithm is specified to wait rather than resolve when the document is not visible — so the diagnostic call never returns if you fire it after sending the user off to a settings window.
- Say the uncertain thing when it is uncertain. Where the pair does not resolve it, "we could not access a camera" is honest and "no camera found" is not.
The bug leaves the engineering column
A wrong error message is a bug somebody can find. A wrong error message that converts into a missed appointment is a bug that has been reclassified as patient behaviour before it reaches an engineer — what travels upward is a no-show rate, and nobody reconciles a no-show rate against an error log.
What does this cost the person on the other end?
In a consumer product, a confusing error message. In a clinical one, an appointment.
A patient who is told their camera was not found has been handed a hardware problem to solve, at the moment they were supposed to be talking to a clinician. The remedy the message implies — find another device, buy a webcam, call someone — takes longer than the appointment slot. The visit is recorded as a no-show, which attributes the failure to the patient, and the number that reaches anyone who could fix it is a no-show rate rather than an error rate.
That accounting matters more than the misdiagnosis itself. A wrong error message is a bug someone can find. A wrong error message that converts into a no-show is a bug that has been reclassified as patient behaviour before it reaches an engineer.
What should the check actually be?
Keep the pre-flight, change what it claims. There is a real use for enumerating devices before you ask: populating a device picker, remembering a previous choice, warning that no camera is listed at all — that last one being a genuinely empty list, which does happen when there is genuinely nothing.
What it may not do is report a diagnosis it cannot support. The three-branch gate above turns "I know nothing yet" into a specific claim about hardware. Delete the branches, keep the enumeration, and move the diagnosis after the rejection where the evidence is.
And when you write the message: the platform gave you a name, not a cause. NotFoundError is
shorthand for "I have nothing to give you", and the reasons it might have nothing include one that is
entirely about permission and nothing about hardware.
FAQ
Is this the same as the user clicking "block" on the permission prompt? No, and they need different messages. A per-site denial in the browser produces a different error name, and the recovery lives in browser UI you do not control — its own problem, and its own article. This piece is about the layer below that: the operating system refusing the browser, which surfaces as absent hardware.
Which browsers does this affect?
We are not publishing a matrix, and the honest reason is that the engines differ here in a way one
row would hide. The NotFoundError mapping is documented in one browser's own bug tracker, on one
operating system. Another engine reports an OS-level refusal as a permission error instead — which
means the pair is diagnostic wherever the name is NotFoundError, and an engine that uses a
permission name for an OS refusal needs the same enumerate-afterwards branch on that name.
Can permissions.query() answer it instead?
No. It reports the site's permission state, and the specification is explicit that a "granted"
state is no guarantee getUserMedia will succeed — there are many other reasons it can fail. An
OS-blocked camera on a site the user has already allowed queries as granted. Where it does answer
something useful, it answers a different article's question:
whether to show a dialog at all.
If the gate is harmless, why change it? Because it is load-bearing in the wrong direction. It does not cause the misdiagnosis, but it spends the call that prevents one, and it encodes the belief that the question was already answered. Both systems that had the gate also had the wrong message.
The three things worth taking away
The error name is not the cause. NotFoundError covers missing hardware and a system-level
refusal — by a deliberate bug fix in one engine, and the platform's own editors say so.
Order decides whether the call is diagnostic. enumerateDevices() before the request answers a
question nobody asked. The same call after the rejection separates the two causes in one line.
Check what your message tells the user to do. If the remedy you imply is "buy hardware", you have committed to the one branch you cannot distinguish — and in a clinical product, the cost of that lands on a patient and is filed under their name.
If your product has a no-show rate that nobody has ever reconciled against its error logs, that is a cheap thing to have looked at.