Why Uint8Array Negative Assignments Wrap to 255
For an input it can coerce to a Number, a
Uint8Arraytruncates the value to an integer and applies mathematical modulo 256, so-1wraps to255instead 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.
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:
- Apply
ToNumber; if conversion succeeds, it produces a JavaScript Number. - Convert
NaN, positive or negativeInfinity,0, and-0to positive0. - Truncate any finite fractional value toward zero.
- Reduce the resulting integer by mathematical modulo
2^8, which is modulo256.
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 value | Integer before modulo | Stored byte |
|---|---|---|
-513 | -513 | 255 |
-257 | -257 | 255 |
-256 | -256 | 0 |
-255 | -255 | 1 |
-1.9 | -1 | 255 |
-1 | -1 | 255 |
0 | 0 | 0 |
255 | 255 | 255 |
256 | 256 | 0 |
257 | 257 | 1 |
4,000,000 | 4,000,000 | 0 |
NaN | 0 | 0 |
Infinity | 0 | 0 |
-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.
Uint8Arraywraps values into the unsigned range0through255.Int8Arrayretains the low eight bits and interprets them in the signed range-128through127.Uint8ClampedArrayclamps values to0through255and 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.
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.
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:
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.