Why Uint8Array Negative Assignments Wrap to 255

Sep 2, 2026·15 min read

For an input it can coerce to a Number, a Uint8Array truncates the value to an integer and applies mathematical modulo 256, so -1 wraps to 255 instead of causing an error.

Out-of-Range Integers Wrap After Truncation

A Uint8Array holds 8-bit unsigned integers. Eight bits provide 256 possible values, numbered from 0 through 255.

When a finite Number is an integer outside that range, assignment does not reject it. The value wraps around the range in either direction. Going one step above 255 returns to 0, while going one step below 0 returns to 255.

A byte has no dead end01… 254255256 positionsthen the pattern repeats−1 step+1 step255 and 0 touch at the wrap boundary
The unsigned byte range loops at the boundary in both directions.

Start with indexed assignment and constructor conversion:

const bytes = new Uint8Array(3);

bytes[0] = -1;
bytes[1] = -256;
bytes[2] = -257;

const fromConstructor = new Uint8Array([-1, 256, 257]);

console.log(...bytes);
console.log(...fromConstructor);
255 0 255
255 0 1

Writing -1 moves one position below 0, so it lands on 255. Writing -256 makes one complete trip around the 256-value range and lands on 0. Writing -257 makes that trip and then moves one position farther, landing on 255.

Positive values follow the same repeating pattern. 256 becomes 0, and 257 becomes 1.

Wrapping maps an integer into the repeating range 0 through 255. It is a conversion rule, not an overflow exception.

The same rule applies whether a value arrives through indexed assignment, an array-like constructor argument, or another operation that writes an element. The broader Arrays guide covers ordinary array behavior; typed arrays add a numeric conversion step to every element write.

How JavaScript Converts a Value to Uint8

The ECMAScript operation behind the conversion is ToUint8. It processes the input in a fixed order:

  1. Apply ToNumber; if conversion succeeds, it produces a JavaScript Number.
  2. Convert NaN, positive or negative Infinity, 0, and -0 to positive 0.
  3. Truncate any finite fractional value toward zero.
  4. Reduce the resulting integer by mathematical modulo 2^8, which is modulo 256.

A BigInt, Symbol, or object whose coercion throws causes assignment to throw before truncation or modulo occurs.

Each step matters. Say you assign -257.9. Truncation happens first, producing -257, and mathematical modulo 256 then produces 255.

Here is that assignment on its own:

const bytes = new Uint8Array(1);

bytes[0] = -257.9;

console.log(bytes[0]);
255

Rounding would have produced -258, but ToUint8 does not round to the nearest integer. It removes the fractional part toward zero. Positive 12.9 becomes 12, and negative -12.9 becomes -12.

The final modulo operation keeps the portion that fits into eight bits. For an integer input, this has the same result as retaining its lowest eight bits, but the Number is converted and truncated before a byte is stored. JavaScript is not clipping the binary representation of the original fractional Number.

Nothing overflows in the exception-producing sense once Number coercion succeeds. The assignment converts the resulting Number and stores it. That behavior is useful for byte-level work, but it can conceal invalid input when your application expects the range check to happen automatically.

These steps are one specific case of JavaScript’s wider type conversion rules. The order is what explains the edge cases.

Negative, Fractional, and Oversized Examples

The conversion becomes predictable once every value goes through the same pipeline.

Assigned valueInteger before moduloStored byte
-513-513255
-257-257255
-256-2560
-255-2551
-1.9-1255
-1-1255
000
255255255
2562560
2572571
4,000,0004,000,0000
NaN00
Infinity00

-513 and -257 both land on 255 because they differ by one or more complete groups of 256. -255 lands on 1, the position one step above the lower wrap boundary.

4,000,000 becomes 0 for an exact reason: 4,000,000 is divisible by 256. The conversion removes every complete group of 256, leaving no remainder.

That does not mean the entire integer has been stored. One Uint8Array element holds one byte. Preserving a value such as 4,000,000 requires a deliberate multi-byte representation, a subject covered by ArrayBuffer and binary arrays.

There is another result that is easy to miss. An assignment expression returns its original right-hand value, while a later array read returns the converted byte:

const bytes = new Uint8Array(1);

const assignmentResult = (bytes[0] = -1);

console.log(assignmentResult);
console.log(bytes[0]);
-1
255

The expression evaluates to -1 because that was its right-hand value. The write converts -1 to 255, so reading bytes[0] returns 255.

Compound assignment has the same split. It computes a result, writes the converted form, and returns the computed result:

const bytes = new Uint8Array([255]);

const additionResult = (bytes[0] += 2);

console.log(additionResult);
console.log(bytes[0]);
257
1

The addition produces 257, which is the value of the compound assignment expression. Storing that result wraps it to 1.

The variable and the array element therefore disagree on purpose. One holds the expression result; the other exposes the stored byte.

Modulo 256 Is Not JavaScript’s % Operator

Mathematical modulo with a positive modulus produces a value from 0 up to one less than that modulus. Mathematical modulo 256 therefore always produces a value from 0 through 255.

JavaScript’s % operator computes a remainder instead. A nonzero remainder keeps the sign of its dividend, so a negative input can produce a negative result:

console.log(-1 % 256);
console.log(-257 % 256);
console.log(257 % 256);
-1
-1
1

Neither -1 nor -257 can be stored as an unsigned byte, so value % 256 does not reproduce ToUint8 for those inputs.

For an integer n, the common non-negative normalization applies % twice:

function modulo256(n) {
  return ((n % 256) + 256) % 256;
}

console.log(modulo256(-257));
console.log(modulo256(-1));
console.log(modulo256(257));
255
255
1

The first remainder may be negative. Adding 256 moves it into a positive interval, and the second remainder maps an already positive multiple such as 256 back to 0.

This formula matches mathematical modulo 256 for integer Numbers. It is not a complete replacement for ToUint8, because ToUint8 also performs Number coercion, converts non-finite values to 0, and truncates fractions toward zero.

For example, applying the formula directly to -1.9 produces a fractional result, while Uint8Array first turns -1.9 into -1 and stores 255. The Numbers guide covers the Number operations around this conversion.

Uint8Array vs Int8Array vs Uint8ClampedArray

All three typed arrays use one byte per element. Uint8Array and Uint8ClampedArray both read the stored byte as an unsigned value from 0 through 255, but use different write conversions; Int8Array additionally interprets the stored bit pattern as signed.

  • Uint8Array wraps values into the unsigned range 0 through 255.
  • Int8Array retains the low eight bits and interprets them in the signed range -128 through 127.
  • Uint8ClampedArray clamps values to 0 through 255 and rounds fractions to the nearest integer, using round-half-to-even when a value is exactly between two integers.

Put the same boundary values into all three arrays:

const values = [-1, 127, 128, 255, 256];

console.log(...new Uint8Array(values));
console.log(...new Int8Array(values));
console.log(...new Uint8ClampedArray(values));
255 127 128 255 0
-1 127 -128 -1 0
0 127 128 255 255

For Uint8Array, -1 wraps to 255 and 256 wraps to 0.

For Int8Array, the bits stored for 255 are read as -1, while the bits stored for 128 are read as -128. The byte has not grown or shrunk. Its signed interpretation has changed.

For Uint8ClampedArray, -1 stops at 0 and 256 stops at 255. Values do not pass through one boundary and reappear at the other.

Fractions reveal a second difference. Uint8Array truncates toward zero, while Uint8ClampedArray rounds to the nearest integer with even-number tie breaking:

const values = [-1.9, 1.5, 2.5, 254.6, 255.9];

console.log(...new Uint8Array(values));
console.log(...new Uint8ClampedArray(values));
255 1 2 254 255
0 2 2 255 255

1.5 becomes 2 in the clamped array. 2.5 also becomes 2, because 2 is the even choice at that exact tie. This is not the same rule as Math.round.

Fractions meet different rulesUint8Array: cut toward zero1231.5Clamped: nearest, with even ties21.52.5Both exact ties choose the even neighbor: 2
At exact halves, clamped conversion chooses the neighboring even integer.

Pick the type according to the stored meaning. Use unsigned bytes when 0 through 255 is the intended repeating representation, signed bytes when each eight-bit pattern should be read in the signed range, and clamped bytes when crossing a boundary should stop at that boundary.

When Wrapping Is Useful and When to Validate

Wrapping is useful when you are deliberately extracting one byte from an integer. Assigning a value to Uint8Array keeps its low eight bits after the required conversion, which fits binary formats built from individual bytes.

DataView.prototype.setUint8 follows the same unsigned eight-bit conversion:

const buffer = new ArrayBuffer(2);
const view = new DataView(buffer);

view.setUint8(0, -1);
view.setUint8(1, 257);

console.log(view.getUint8(0), view.getUint8(1));
255 1

The first write stores 255, and the second stores 1. DataView gives you explicit byte offsets, but setUint8 does not add range validation.

Storing one byte is also different from encoding an integer across several bytes. Assigning 4,000,000 to one element stores 0; it does not preserve the other bits somewhere nearby. You must choose a multi-byte layout when the entire number matters.

Storage must have room for every byte4,000,0000x003D0900one slotchosen layoutone Uint8 elementlow byte00the other bytes arenot storedfour-byte layout003D0900all four positions areexplicitBytes do not spill automatically into neighboring elements
A one-byte write keeps only 00; a chosen multi-byte layout can retain the whole integer.

Wrapping is the wrong behavior when a value represents an application constraint such as a percentage, channel number, or validated byte from user input. Check the value before assignment in that case.

Use one guard at the boundary:

Put the rule before the conversionincomingvaluefinite integer?0 through 255?check before writingnoyesthrow RangeErrorstore the byteInvalid data never reaches the wrapping operation
Validate before the typed-array write when out-of-range data should be rejected.
function requireByte(value) {
  if (
    typeof value !== 'number' ||
    !Number.isFinite(value) ||
    !Number.isInteger(value) ||
    value < 0 ||
    value > 255
  ) {
    throw new RangeError('Expected an integer from 0 through 255');
  }

  return value;
}

const bytes = new Uint8Array(2);

bytes[0] = requireByte(42);
bytes[1] = requireByte(255);

console.log(...bytes);

try {
  bytes[0] = requireByte(-1);
} catch (error) {
  console.log(error.name);
}
42 255
RangeError

The guard rejects NaN, infinities, fractions, negative numbers, and integers above 255. The typed array then handles storage, while the application owns the validity rule.

That separation is the practical choice. Use wrapping when you mean byte conversion. Validate first when wrapping would hide a mistake.

The Complete bundle carries this byte-level model into the wider sequence on numbers, binary data, strings, and the browser platform.

Frequently asked questions

Why does assigning -1 to a Uint8Array produce 255?
Uint8Array stores unsigned 8-bit integers from 0 through 255. JavaScript truncates the input and applies mathematical modulo 256, which maps -1 to 255.
Does Uint8Array clamp negative numbers to zero?
No. Uint8Array wraps negative and oversized integers into the range 0 through 255. Use Uint8ClampedArray when values should clamp at the two boundaries instead.
Is value % 256 the same as Uint8Array conversion?
Not for negative inputs, because JavaScript's % operator returns a signed remainder. For inputs that ToNumber can process without throwing, Uint8Array also handles non-finite values and truncates fractions before applying mathematical modulo 256. A BigInt, Symbol, or coercion that throws makes the assignment throw before those steps.
What happens when a fraction is assigned to a Uint8Array?
The fractional part is truncated toward zero before the integer wraps. Assigning -1.9 therefore converts it to -1 and stores 255.
How can JavaScript reject values instead of wrapping them?
Check that the input is a finite integer between 0 and 255 before assigning it. Throwing a RangeError at that boundary prevents silent conversion from hiding invalid application data.