Every entry in $_FILES carries an error value, and reading it correctly turns "the upload did not work" into a specific, fixable cause. There are eight defined constants, one gap in the numbering, and several failure modes that produce no code at all.
The codes at a glance
| Constant | Value | Meaning |
|---|---|---|
UPLOAD_ERR_OK | 0 | The file uploaded successfully. |
UPLOAD_ERR_INI_SIZE | 1 | Larger than upload_max_filesize. |
UPLOAD_ERR_FORM_SIZE | 2 | Larger than the form's MAX_FILE_SIZE hidden field. |
UPLOAD_ERR_PARTIAL | 3 | Only part of the file arrived. |
UPLOAD_ERR_NO_FILE | 4 | No file was submitted. |
UPLOAD_ERR_NO_TMP_DIR | 6 | The temporary folder is missing. |
UPLOAD_ERR_CANT_WRITE | 7 | Writing to disk failed. |
UPLOAD_ERR_EXTENSION | 8 | A PHP extension stopped the upload. |
There is no error 5. The value was used briefly during PHP 5.x development and then withdrawn, and the gap has been there ever since. If you are switching on these values, do not write a case for 5 — and if something in your logs reports 5, it is not coming from PHP's upload handling.
What each one actually means
1 — UPLOAD_ERR_INI_SIZE
The file exceeded upload_max_filesize (default 2M). The upload is rejected per-file, so in a batch the other files can still succeed. Raise the directive, remembering that post_max_size must stay comfortably above it — see the size limits article.
2 — UPLOAD_ERR_FORM_SIZE
The file exceeded a MAX_FILE_SIZE hidden field in the form:
<input type="hidden" name="MAX_FILE_SIZE" value="2097152">
This is a client-side courtesy only — the browser is trusted to honour it, and anyone can edit or remove it. Treat code 2 as a hint, never as a security control, and enforce the real limit server-side.
3 — UPLOAD_ERR_PARTIAL
The connection ended before the whole file arrived: the user navigated away, the network dropped, a proxy timed out, or the browser was closed mid-transfer. There is nothing to fix in configuration — discard the fragment and ask the user to retry. Frequent code 3 on large files is the signal to move to chunked uploads, where only the failed chunk is repeated.
4 — UPLOAD_ERR_NO_FILE
The field was submitted with nothing chosen. Perfectly normal for an optional input, so treat it as "skip this one" rather than an error:
if ($file['error'] === UPLOAD_ERR_NO_FILE) {
continue; // optional field, nothing selected
}
6 — UPLOAD_ERR_NO_TMP_DIR
PHP has nowhere to put the incoming file. Either upload_tmp_dir points somewhere that does not exist, or it is unset and the system temp directory is unavailable — common in hardened or containerised environments, and on Windows where the path may not exist for the service account. Check what PHP thinks:
var_dump(ini_get('upload_tmp_dir'), sys_get_temp_dir(), is_writable(sys_get_temp_dir()));
7 — UPLOAD_ERR_CANT_WRITE
The temp directory exists but the write failed: permissions, a full disk, a quota, or SELinux/AppArmor blocking the web-server user. Check free space first — a full /tmp is the usual culprit and produces no other symptom.
8 — UPLOAD_ERR_EXTENSION
A loaded PHP extension aborted the upload. It is deliberately vague, because PHP does not record which one. Realistically it is a security or antivirus module; check the extensions list and the error log around the same timestamp.
Turning codes into messages
function upload_error_message(int $code): string
{
return match ($code) {
UPLOAD_ERR_OK => 'Uploaded successfully.',
UPLOAD_ERR_INI_SIZE => 'The file is larger than this server allows.',
UPLOAD_ERR_FORM_SIZE => 'The file is larger than the form allows.',
UPLOAD_ERR_PARTIAL => 'The upload was interrupted. Please try again.',
UPLOAD_ERR_NO_FILE => 'No file was selected.',
UPLOAD_ERR_NO_TMP_DIR => 'Server error: no temporary folder.',
UPLOAD_ERR_CANT_WRITE => 'Server error: could not write the file.',
UPLOAD_ERR_EXTENSION => 'The upload was blocked by a server extension.',
default => 'Unknown upload error (' . $code . ').',
};
}
Codes 6, 7 and 8 are server faults, not user mistakes. Log those with context and show a generic apology — the user cannot act on "no temporary folder", and the detail belongs in your log, not on the page. Codes 1 to 4 are worth stating plainly, because the user genuinely can retry with a smaller file.
match with a switch. The constants themselves have been stable since PHP 5.2, so the logic is otherwise unchanged.Failures that produce no error code at all
The most confusing upload bugs are the ones where there is nothing in $_FILES to inspect:
- Missing
enctype="multipart/form-data"—$_FILESis empty; the file name shows up in$_POSTinstead. - The POST exceeded
post_max_size— PHP discards the whole body, so both$_POSTand$_FILESare empty. Detect it by checkingCONTENT_LENGTHon an otherwise empty POST. - More files than
max_file_uploads— the surplus is dropped silently, with no entries and no error. - The web server rejected the request — Nginx 413 or IIS 404.13, before PHP ran at all.
A defensive handler checks for these before iterating:
if ($_SERVER['REQUEST_METHOD'] === 'POST' && empty($_FILES)) {
$len = (int) ($_SERVER['CONTENT_LENGTH'] ?? 0);
if ($len > 0) {
error_log("Upload discarded: {$len} bytes exceeded post_max_size");
// report a size error to the user
} else {
// form was posted without a file field, or without the right enctype
}
}
Fewer of these to handle
Most of the codes above describe limits that only exist because the whole file rides in one POST. PHP File Uploader validates size and type in the browser before transferring anything, streams the data in parts rather than one request, and reports a specific message per file — so users see "too large" the moment they pick the file, not after a two-minute wait. The validation demo shows the behaviour, and the API documentation lists the server-side events.
