← Math in Practice
Concept Math in engineering decisions

Signed numbers, overflow, and fixed-width values

A number that fits in memory can still be the wrong number to trust.

An upload service reports a negative byte count after a large transfer. The deployment dashboard is green, requests are still completing, and the displayed count flips near two gigabytes. Is this a bad subtraction, a unit conversion, a counter that is too small, or a number that lost precision on the way through the system?

The judgment to keep

Find the first operation whose mathematical result crosses the representation's range. Then distinguish fixed-width overflow from floating-point precision loss, and verify the value at the boundary where it is stored, converted, or serialized.

TypeScriptGo Signed ranges · two's complement · fixed-width overflow · safe integer precision
01 / Read the negative count

The report is a clue. Find out what was counted and where.

This is an illustrative upload incident. A worker adds each received chunk to a signed 32-bit byte counter. The count is positive until it approaches 2,147,483,647, then a later update makes the reported total negative. A 32-bit signed integer has a finite range; the service may be doing exactly the arithmetic its type defines, while violating the application's expectation that a byte count stays nonnegative.

The scenario does not establish the cause of any real outage. A negative dashboard value could also come from a bad unit conversion, an incorrect delta, a decoder bug, or a metric label collision. Start by locating the raw counter, its declared type, every update and conversion, and the first timestamp at which stored state differs from the expected cumulative bytes.

Case file / Upload workerA counter crosses its signed maximum during accumulation.
Observed clue
Reported cumulative bytes become negative near 2 GiB.
Illustrative starting count
2,147,000,000 bytes
Next chunk
1 MiB = 1,048,576 bytes
Question
What result should the type hold, and what should the service do at its limit?
02 / Map the signed range

One bit chooses the sign, leaving the rest to encode magnitude around zero.

A signed n-bit two's-complement integer has 2ⁿ bit patterns. The top bit has weight −2ⁿ⁻¹; the remaining bits have positive weights 2⁰ through 2ⁿ⁻². Its range is therefore −2ⁿ⁻¹ through 2ⁿ⁻¹ − 1. There is one more negative value than positive values because zero takes one of the nonnegative patterns.

For 8 bits, 0111 1111₂ = 127, while 1000 0000₂ = −128. Add one to 127 in an 8-bit signed representation and the bits roll to 1000 0000₂, interpreted as −128. In two's-complement arithmetic, negating a value means invert every bit and add one; that is why the negative endpoint has no positive counterpart in the same width.

For the 32-bit upload counter, minimum is −2,147,483,648 and maximum is 2,147,483,647. The illustrated update is not a close-call rounding issue: its exact mathematical sum is beyond the type's largest representable value.

General signed n-bit range−2ⁿ⁻¹ … 2ⁿ⁻¹ − 1
Signed int32 range−2,147,483,648 … 2,147,483,647
Exact illustrative sum2,147,000,000 + 1,048,576 = 2,148,048,576
Above int32 max2,148,048,576 − 2,147,483,647 = 564,929
03 / Separate overflow from precision

Go's bounded integers and TypeScript's Number fail differently.

TypeScript's number is JavaScript's IEEE 754 binary64 floating-point type. It is not a fixed-width signed integer. Every integer is exactly represented only from −(2⁵³ − 1) through 2⁵³ − 1, or ±9,007,199,254,740,991. Beyond that safe range, a value can still be finite, but adjacent integers may map to the same Number. For example, Number.MAX_SAFE_INTEGER + 1 and + 2 both evaluate to 9,007,199,254,740,992.

That is precision loss, not 32-bit wrap. A TypeScript counter holding 2.148 billion bytes is represented exactly by Number; the familiar int32 boundary does not change Number arithmetic. Use Number.isSafeInteger for integer values that must stay exact, or use bigint for larger exact integer arithmetic. BigInt and Number arithmetic cannot be mixed implicitly. If large integers arrive as JSON numeric tokens, precision can already be lost during parsing; a string representation may be needed at that boundary.

Go's int32 and int64 are fixed-width signed integers. The language specification defines signed overflow as deterministic; it does not panic. The 32-bit result wraps as shown above. Go's architecture-sized int may be 32 or 64 bits depending on the target, so protocol and storage boundaries should use explicit widths when they matter. A conversion to a narrower integer type can also truncate, so validate before converting.

TypeScript / NumberRounded integer precision

At 2⁵³, neighboring integer values stop being distinguishable. Number does not wrap at int32 boundaries.

Go / int32Defined fixed-width overflow

At 2,147,483,647 + 1, signed arithmetic yields −2,147,483,648. No runtime panic signals the application error.

Application contractRange is not validity

A value can fit the machine type and still exceed a product, protocol, allocation, or storage limit.

04 / Move the boundary

Predict the result before you let the calculator show it.

Choose a small signed width and add a delta. The lab calculates the exact mathematical sum first, then shows its two's-complement signed interpretation at the selected width. This models fixed-width bit arithmetic only; a production program should normally detect an invalid update rather than rely on wrap.

Fixed-width signed addition
Representable range-128 … 127
Exact mathematical sum128
Would fit the selected signed width?No — outside the range
Fixed-width signed interpretation-128

The wrap result is a representation demonstration. For a counter, reject or handle the update when the intended value is out of range.

Now inspect JavaScript Number precision
Result9007199254740992.0
Exact integer arithmetic allowed?No — input or result is outside the safe integer range

Try starting at 9,007,199,254,740,991 and add 1, then add 2. The results display the same Number because distinct mathematical integers are no longer distinguishable there. For actual exact calculations use BigInt (or decimal strings before conversion).

05 / Compare the languages

Check the update at the point where the value can cross its limit.

The TypeScript helper rejects non-safe integers before addition and checks the result after addition. The Go helper checks the int64 bounds before evaluating the sum; its separate int32 example reproduces the upload counter's wrapped result. Explicit types help describe bounds, while checked arithmetic enforces the application's policy.

Compare the same boundary question in TypeScript and Go.

The snippets show distinct numeric models: safe integer validation for Number, checked int64 addition, and defined fixed-width overflow.

TypeScriptInteger range and precision examples in TypeScript and Go
fixed-width.ts
export type SignedWidth = 8 | 16 | 32;

export function isSafeByteCount(value: number): boolean {
	return Number.isSafeInteger(value) && value >= 0;
}

export function checkedAdd(current: number, delta: number): number | undefined {
	if (!Number.isSafeInteger(current) || !Number.isSafeInteger(delta)) return undefined;
	const result = current + delta;
	return Number.isSafeInteger(result) ? result : undefined;
}

export function addSigned(width: SignedWidth, current: bigint, delta: bigint): bigint {
	const modulus = 1n << BigInt(width);
	const signBit = 1n << BigInt(width - 1);
	const unsigned = (((current + delta) % modulus) + modulus) % modulus;
	return unsigned >= signBit ? unsigned - modulus : unsigned;
}

export const example = {
	width: 32 as SignedWidth,
	max: (1n << 31n) - 1n,
	chunk: 1_048_576n,
	before: 2_147_000_000n
};
GoInteger range and precision examples in TypeScript and Go
fixed-width.go
package main

import (
	"errors"
	"math"
)

func checkedAddInt64(current, delta int64) (int64, error) {
	if delta > 0 && current > math.MaxInt64-delta {
		return 0, errors.New("byte count exceeds int64 maximum")
	}
	if delta < 0 && current < math.MinInt64-delta {
		return 0, errors.New("byte count falls below int64 minimum")
	}
	return current + delta, nil
}

func demonstrateInt32Wrap() (int32, int32) {
	before := int32(2_147_000_000)
	chunk := int32(1_048_576)
	return before, before + chunk // defined signed overflow: -2_146_918_720
}
06 / Diagnose the first bad update

Follow the value, not just the final graph.

For the current language semantics, see the TypeScript Handbook's everyday types (which explains that TypeScript shares JavaScript runtime behavior), MDN's Number.MAX_SAFE_INTEGER, and the Go specification sections on integer overflow and conversions. Go's math integer-limit constants provide auditable bounds such as MaxInt32 and MaxInt64. These references describe language behavior; your application still has to decide which values are valid.