A barcode can look perfectly valid and still encode an invalid number.
That is because retail barcode formats such as EAN-13 and UPC-A do more than render black bars.
They also include a check digit.
The check digit helps detect common mistakes in the numeric identifier.
Once you understand the algorithm, you can build a validator in a few lines of JavaScript.
What a check digit does
A check digit does not encrypt the number.
It does not prove that a product exists.
It does not tell you who owns the barcode.
It is a small integrity check.
GS1 describes the final digit of a barcode number as a check digit used to make sure the number is correctly composed.
Official reference:
EAN-13 structure
EAN-13 contains 13 digits.
Conceptually:
12 data digits
+ 1 check digit
= 13 digits
Example:
4006381333931
Here:
400638133393
is the first 12 digits.
The final:
1
is the check digit.
Let's calculate it.
EAN-13 check digit algorithm
Take the first 12 digits.
Starting from the left:
- multiply digits in odd positions by
1 - multiply digits in even positions by
3 - add the results
- find what digit must be added to reach the next multiple of 10
Using:
4 0 0 6 3 8 1 3 3 3 9 3
Weights:
1 3 1 3 1 3 1 3 1 3 1 3
Multiply:
4×1 = 4
0×3 = 0
0×1 = 0
6×3 = 18
3×1 = 3
8×3 = 24
1×1 = 1
3×3 = 9
3×1 = 3
3×3 = 9
9×1 = 9
3×3 = 9
Sum:
4 + 0 + 0 + 18
+ 3 + 24 + 1 + 9
+ 3 + 9 + 9 + 9
= 89
We need the next multiple of 10:
90
So:
90 - 89 = 1
Check digit:
1
Full EAN-13:
4006381333931
A clean JavaScript implementation
function ean13CheckDigit(
first12
) {
if (!/^\d{12}$/.test(first12)) {
throw new Error(
"EAN-13 payload must be 12 digits"
);
}
const sum =
[...first12]
.map(Number)
.reduce(
(total, digit, index) => {
const weight =
index % 2 === 0
? 1
: 3;
return (
total +
digit * weight
);
},
0
);
return (
10 - (sum % 10)
) % 10;
}
Test it:
console.log(
ean13CheckDigit(
"400638133393"
)
);
// 1
Generate a complete EAN-13 number
function completeEan13(first12) {
const checkDigit =
ean13CheckDigit(first12);
return (
first12 +
checkDigit
);
}
console.log(
completeEan13(
"400638133393"
)
);
// 4006381333931
Validate an existing EAN-13
function isValidEan13(value) {
if (!/^\d{13}$/.test(value)) {
return false;
}
const payload =
value.slice(0, 12);
const expected =
ean13CheckDigit(payload);
const actual =
Number(value[12]);
return expected === actual;
}
Test:
console.log(
isValidEan13(
"4006381333931"
)
);
// true
A more generic Mod-10 function
EAN/UPC families use related Mod-10 weighting logic.
You can write a generic helper:
function calculateGs1CheckDigit(
payload
) {
if (!/^\d+$/.test(payload)) {
throw new Error(
"Payload must contain digits only"
);
}
const digits =
[...payload]
.map(Number)
.reverse();
const sum =
digits.reduce(
(total, digit, index) => {
const weight =
index % 2 === 0
? 3
: 1;
return (
total +
digit * weight
);
},
0
);
return (
10 - (sum % 10)
) % 10;
}
The reverse-first approach makes it easier to reason from the rightmost payload digit.
For EAN-13, pass the 12 data digits.
For UPC-A, pass the 11 data digits.
UPC-A example
UPC-A is commonly represented as:
11 data digits
+ 1 check digit
= 12 digits
So:
function completeUpcA(first11) {
if (!/^\d{11}$/.test(first11)) {
throw new Error(
"UPC-A payload must be 11 digits"
);
}
return (
first11 +
calculateGs1CheckDigit(
first11
)
);
}
Validation:
function isValidUpcA(value) {
if (!/^\d{12}$/.test(value)) {
return false;
}
const payload =
value.slice(0, 11);
const expected =
calculateGs1CheckDigit(
payload
);
return (
expected ===
Number(value[11])
);
}
Why modulo 10?
The check digit is chosen so the weighted total plus the check digit ends on a multiple of 10.
If:
sum % 10 = 0
then the check digit is:
0
That is why the formula is usually written:
(10 - (sum % 10)) % 10
The second % 10 handles the case where the sum is already divisible by 10.
Without it:
10 - 0 = 10
which is not a valid single digit.
Check digits are validation, not uniqueness
This is an important distinction.
Suppose your function says:
isValidEan13("...") === true
That only means:
the number passes the mathematical check-digit rule.
It does not automatically mean:
- the number was licensed correctly
- the product exists
- the manufacturer matches
- the identifier is currently assigned
- a marketplace will accept it
For real GS1 identity verification, use appropriate GS1 services and rules.
Barcode data vs barcode image
Another common confusion:
valid number
≠
valid barcode image
Once the data is correct, you still have to render the symbol correctly.
That includes things such as:
- bar pattern
- quiet zones
- dimensions
- readable contrast
- correct human-readable text
- print scaling
So there are really two layers:
Layer 1:
identifier validation
Layer 2:
barcode rendering
This article is primarily about Layer 1.
Why this matters in e-commerce tooling
If you process hundreds of products from a spreadsheet, it is much better to catch invalid identifiers before creating hundreds of barcode images.
A useful workflow:
CSV / Excel
↓
validate values
↓
calculate missing check digits
↓
flag invalid rows
↓
render barcode images
↓
download
Validation belongs before rendering.
Batch generation adds product-level complexity
Generating one barcode is easy.
Generating 500 introduces new concerns:
- filenames
- duplicate values
- invalid rows
- format selection
- progress
- ZIP packaging
- spreadsheet mapping
- error reporting
That is why a barcode generator quickly becomes a workflow tool.
I built these kinds of operational flows into BatchSet's Barcode Generator.
The current tool supports formats including:
- EAN-13
- UPC-A
- EAN-8
- UPC-E
- Code 128
and handles check digits for the relevant retail formats exposed in the interface.
If you need the actual scannable output rather than only the checksum code:
Generate a barcode with BatchSet
Useful test cases
Do not test only valid inputs.
const cases = [
"",
"123",
"abcdefghijkl",
"400638133393",
"000000000000",
"999999999999"
];
for (const value of cases) {
try {
console.log(
value,
ean13CheckDigit(value)
);
} catch (error) {
console.log(
value,
error.message
);
}
}
Also test validation with:
correct check digit
incorrect check digit
leading zeros
non-digit input
too few digits
too many digits
Leading zeros are especially important because barcode identifiers are strings, not ordinary numbers.
Do not do this:
Number("012345678901")
if preserving the exact identifier matters.
Keep the value as a string.
Final takeaway
A retail barcode is not just black lines.
Before you render anything, validate the identifier.
For EAN-13 and UPC-A, the check-digit algorithm is small enough to understand and implement directly:
weighted sum
→ modulo 10
→ final check digit
That little validation step can prevent a surprising amount of bad data from moving deeper into an e-commerce or inventory workflow.












