DEV Community

Cover image for EAN-13 and UPC-A Check Digits Explained: Build a Barcode Validator in JavaScript
Muhaymin Bin Mehmood
Muhaymin Bin Mehmood

Posted on

EAN-13 and UPC-A Check Digits Explained: Build a Barcode Validator in JavaScript

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:

GS1 Check Digit Calculator


EAN-13 structure

EAN-13 contains 13 digits.

Conceptually:

12 data digits
+ 1 check digit
= 13 digits
Enter fullscreen mode Exit fullscreen mode

Example:

4006381333931
Enter fullscreen mode Exit fullscreen mode

Here:

400638133393
Enter fullscreen mode Exit fullscreen mode

is the first 12 digits.

The final:

1
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Weights:

1 3 1 3 1 3 1 3 1 3 1 3
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Sum:

4 + 0 + 0 + 18
+ 3 + 24 + 1 + 9
+ 3 + 9 + 9 + 9
= 89
Enter fullscreen mode Exit fullscreen mode

We need the next multiple of 10:

90
Enter fullscreen mode Exit fullscreen mode

So:

90 - 89 = 1
Enter fullscreen mode Exit fullscreen mode

Check digit:

1
Enter fullscreen mode Exit fullscreen mode

Full EAN-13:

4006381333931
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Test it:

console.log(
  ean13CheckDigit(
    "400638133393"
  )
);

// 1
Enter fullscreen mode Exit fullscreen mode

Generate a complete EAN-13 number

function completeEan13(first12) {
  const checkDigit =
    ean13CheckDigit(first12);

  return (
    first12 +
    checkDigit
  );
}

console.log(
  completeEan13(
    "400638133393"
  )
);

// 4006381333931
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Test:

console.log(
  isValidEan13(
    "4006381333931"
  )
);

// true
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

So:

function completeUpcA(first11) {
  if (!/^\d{11}$/.test(first11)) {
    throw new Error(
      "UPC-A payload must be 11 digits"
    );
  }

  return (
    first11 +
    calculateGs1CheckDigit(
      first11
    )
  );
}
Enter fullscreen mode Exit fullscreen mode

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])
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

then the check digit is:

0
Enter fullscreen mode Exit fullscreen mode

That is why the formula is usually written:

(10 - (sum % 10)) % 10
Enter fullscreen mode Exit fullscreen mode

The second % 10 handles the case where the sum is already divisible by 10.

Without it:

10 - 0 = 10
Enter fullscreen mode Exit fullscreen mode

which is not a valid single digit.


Check digits are validation, not uniqueness

This is an important distinction.

Suppose your function says:

isValidEan13("...") === true
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Also test validation with:

correct check digit
incorrect check digit
leading zeros
non-digit input
too few digits
too many digits
Enter fullscreen mode Exit fullscreen mode

Leading zeros are especially important because barcode identifiers are strings, not ordinary numbers.

Do not do this:

Number("012345678901")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

That little validation step can prevent a surprising amount of bad data from moving deeper into an e-commerce or inventory workflow.

Top comments (0)