← Back to Blog

Understanding Character Encodings: A Complete Guide

A comprehensive exploration of character encodings - from ASCII to UTF-8 and beyond. Learn how text is represented in computers and how to debug those frustrating encoding issues.

Emma Taylor
Emma TaylorDeveloper Advocate & Technical Writer

Understanding Character Encodings: A Complete Guide

If you've ever seen garbled text on a website, question marks where characters should be, or the infamous "mojibake" where text turns into a seemingly random string of symbols, you've encountered character encoding issues. These problems can be frustrating, but understanding how character encodings work will help you prevent and fix them.

In this guide, we'll start from the very beginning and work our way through everything you need to know about character encodings. Don't worry if this topic seems intimidating - we'll take it step by step, and by the end, you'll have a solid foundation for handling text in any programming context.

What Is a Character Encoding?

At its core, a computer only understands numbers. It doesn't inherently know what the letter "A" is or how to display a smiley face emoji. A character encoding is simply a mapping that tells the computer "when you see the number 65, display the letter A."

Think of it like a codebook. Both the sender and receiver need to agree on which codebook they're using. If I send you a message using one codebook and you decode it with a different one, you'll get gibberish.

Let's make this concrete. Imagine we create a simple encoding where:

  • 1 = A
  • 2 = B
  • 3 = C
  • ...and so on

If I send you the numbers "8 5 12 12 15", you'd look them up and get "HELLO". But if your codebook was different - say, shifted by one - you'd get completely different letters.

This is essentially what happens with real character encodings, just on a larger and more standardized scale.

ASCII: Where It All Began

In the early days of computing, there was chaos. Different computer manufacturers used different encodings, making it impossible to share text between systems. In 1963, the American Standard Code for Information Interchange (ASCII) was developed to solve this problem.

How ASCII Works

ASCII uses 7 bits to represent characters, giving us 128 possible values (0-127). These are divided into:

  • 0-31: Control characters (things like newline, tab, and bell)
  • 32-126: Printable characters (letters, numbers, punctuation)
  • 127: The delete character

Here are some key ASCII values you might encounter:

CharacterDecimalBinary
Space320100000
0480110000
A651000001
a971100001

Notice something interesting? Uppercase letters start at 65, and lowercase letters start at 97. The difference is exactly 32, which means converting between cases is just flipping a single bit. This wasn't an accident - the designers made this choice deliberately to simplify case conversion.

ASCII's Limitations

ASCII worked well for American English, but the world has many more languages. What about accented characters like "e" or "n"? What about non-Latin alphabets like Greek, Arabic, or Chinese?

With only 128 characters available, ASCII simply couldn't represent the full richness of human writing systems. Something more was needed.

Extended ASCII and Code Pages

The first attempt to solve ASCII's limitations was to use that extra bit. Since computers typically work with 8-bit bytes anyway, why not use all 256 possible values?

This gave rise to "extended ASCII" - but here's where things got messy. There was no single standard for what characters 128-255 should represent. Instead, different regions created different "code pages":

  • ISO-8859-1 (Latin-1): Covered Western European languages
  • ISO-8859-5: Covered Cyrillic languages
  • Windows-1252: Microsoft's Western European encoding
  • And many more...

This created a new problem. A document written in one code page would display incorrectly if opened with a different code page. If you've ever opened a text file and seen "cafe" instead of "cafe", you've experienced this firsthand - the file was saved in one encoding but opened with another.

Unicode: One Encoding to Rule Them All

By the late 1980s, it was clear that the code page approach was unsustainable. The Unicode Consortium was formed to create a single character set that could represent every character in every human writing system.

The Unicode Character Set

Unicode assigns a unique number (called a "code point") to every character. These code points are written in hexadecimal with a "U+" prefix. For example:

  • U+0041: Latin Capital Letter A
  • U+00E9: Latin Small Letter E with Acute (e)
  • U+4E2D: CJK Unified Ideograph (the Chinese character for "middle")
  • U+1F600: Grinning Face emoji

Unicode currently defines over 149,000 characters covering 161 scripts. It includes not just modern writing systems, but historical scripts, mathematical symbols, musical notation, and yes, thousands of emoji.

Important Distinction: Character Set vs. Encoding

Here's a crucial point that often causes confusion: Unicode is a character set, not an encoding. Unicode tells us that the code point U+0041 represents the letter "A", but it doesn't specify how to store that number in a file.

Think of it this way: Unicode is the codebook that says what number corresponds to what character. The encoding is the format for writing those numbers down.

This is where UTF-8, UTF-16, and other encodings come in.

UTF-8: The Modern Standard

UTF-8 (Unicode Transformation Format - 8-bit) has become the dominant encoding on the web, and for good reason. It's clever, efficient, and backward compatible with ASCII.

How UTF-8 Works

UTF-8 is a variable-width encoding. Characters use between 1 and 4 bytes depending on their code point:

Code Point RangeBytesBit Pattern
U+0000 to U+007F10xxxxxxx
U+0080 to U+07FF2110xxxxx 10xxxxxx
U+0800 to U+FFFF31110xxxx 10xxxxxx 10xxxxxx
U+10000 to U+10FFFF411110xxx 10xxxxxx 10xxxxxx 10xxxxxx

Let's work through an example. The letter "e" (U+00E9) falls in the second range, so it needs 2 bytes.

The code point in binary is: 11101001

We need to fit this into the pattern: 110xxxxx 10xxxxxx

Splitting up the bits: 110 00011 10 101001

In hexadecimal, that's: C3 A9

So when you save a file containing "e" in UTF-8, those two bytes (C3 A9) are what actually get written to disk.

Why UTF-8 Is Brilliant

There are several design decisions that make UTF-8 particularly elegant:

ASCII Compatibility: Any valid ASCII text is also valid UTF-8. The first 128 characters use identical byte values. This means billions of existing ASCII documents didn't need to be converted.

Self-Synchronization: You can always tell where you are in a UTF-8 stream. If a byte starts with 0, it's a single-byte character. If it starts with 10, it's a continuation byte. If it starts with 110, 1110, or 11110, it's the start of a multi-byte sequence. This makes it possible to recover from errors and to jump into the middle of a stream.

No Null Bytes: Except for the actual NULL character (U+0000), UTF-8 never produces a zero byte. This matters because many programming languages use null bytes as string terminators.

Sorting: UTF-8 encoded strings sort correctly when using simple byte-by-byte comparison for ASCII characters.

UTF-16: Another Approach

UTF-16 uses 16-bit code units. Characters in the Basic Multilingual Plane (BMP, U+0000 to U+FFFF) use a single 16-bit unit. Characters outside this range use two 16-bit units called a "surrogate pair."

When You'll Encounter UTF-16

UTF-16 is used internally by:

  • Windows operating systems
  • Java and .NET strings
  • JavaScript (sort of - it's complicated)
  • Many XML parsers

The advantage of UTF-16 is that most commonly used characters (including all of Chinese, Japanese, and Korean) fit in a single 16-bit unit. This can make string operations faster for text-heavy applications in these languages.

The disadvantage is that it's not ASCII compatible, and you have to deal with byte order issues (more on that shortly).

The Byte Order Mark (BOM)

When you have multi-byte values, you need to know the byte order. Is the most significant byte first (big-endian) or last (little-endian)?

For example, the character A (U+0041) in UTF-16 could be stored as:

  • 00 41 (big-endian)
  • 41 00 (little-endian)

The Byte Order Mark (BOM) is a special character (U+FEFF) placed at the beginning of a file to indicate the byte order. When encoded:

  • FE FF indicates big-endian
  • FF FE indicates little-endian

BOM in UTF-8

UTF-8 doesn't need a byte order mark because it's always the same byte order. However, some applications (notably Windows Notepad) add a UTF-8 BOM (EF BB BF) to files anyway.

This BOM is usually harmless, but it can cause issues:

  • Shell scripts may fail if the BOM comes before the shebang
  • PHP files may output unexpected characters
  • Concatenating files can leave BOMs in the middle

My recommendation: avoid adding a BOM to UTF-8 files unless you have a specific reason to include one.

Emoji: A Modern Encoding Challenge

Emoji deserve special mention because they've pushed character encoding systems in interesting ways.

The first emoji were added to Unicode 6.0 in 2010. Since then, the emoji catalog has exploded. As of Unicode 15.1, there are over 3,700 emoji.

Why Emoji Are Complicated

Many emoji are outside the Basic Multilingual Plane, meaning they require:

  • 4 bytes in UTF-8
  • A surrogate pair in UTF-16

But it gets more interesting. Many emoji are actually sequences of multiple code points:

Skin Tone Modifiers: The waving hand emoji followed by a skin tone modifier creates a variation. That's two code points rendered as one visible character.

Family Emoji: The family emoji can be composed of multiple person emoji joined with Zero Width Joiner (ZWJ) characters. The family emoji might be 7 code points: woman, ZWJ, woman, ZWJ, girl, ZWJ, boy.

Flag Emoji: Flags are pairs of "regional indicator" characters. The US flag is U+1F1FA U+1F1F8 (regional indicators for U and S).

This means you can't simply count code points to count characters, and you certainly can't count bytes. If you're doing string manipulation on text that might contain emoji, you need to be aware of these complications.

Common Encoding Issues and How to Fix Them

Now let's get practical. Here are the encoding issues you're most likely to encounter and how to debug them.

Mojibake: The Classic Symptom

Mojibake is when you see garbled characters like "cafe" instead of "cafe". This happens when text is decoded with the wrong encoding.

How to fix it: Determine the original encoding of the text and decode it correctly. If you see characters from the Latin-1 range where accented characters should be, the text is probably UTF-8 being interpreted as Latin-1.

Question Marks or Replacement Characters

If you see ? or (U+FFFD) where characters should be, the software couldn't interpret certain bytes.

How to fix it: This often means data was corrupted or truncated during an encoding conversion. Check your data pipeline for places where encoding might be changed or bytes might be lost.

Double Encoding

Sometimes text gets encoded twice. For example, UTF-8 text might be incorrectly converted from Latin-1 to UTF-8 again, resulting in things like "café" instead of "cafe".

How to fix it: You'll need to reverse the double encoding. Encode as Latin-1 (to get the original UTF-8 bytes back), then decode as UTF-8.

Database Encoding Issues

Databases have their own encoding settings at the server, database, table, and even column level. Make sure these are all consistent - ideally, everything should be UTF-8 (specifically, utf8mb4 in MySQL if you need emoji support).

Also check the connection encoding. In MySQL, you might need to run SET NAMES utf8mb4 after connecting.

Best Practices for Working with Encodings

Let me share some guidelines that will help you avoid encoding headaches:

Always use UTF-8: Unless you have a specific reason not to, UTF-8 should be your default encoding for files, databases, and network communication.

Declare your encoding explicitly: In HTML, always include <meta charset="UTF-8">. In Python, use encoding declarations. In HTTP, include Content-Type: text/html; charset=utf-8.

Handle encoding at the boundaries: Decode text to Unicode when it enters your program (from files, network, etc.) and encode it when it leaves. Work with Unicode strings internally.

Be careful with string length: In many languages, the "length" of a string might give you bytes, code units, code points, or grapheme clusters - and these can all be different numbers for the same text.

Test with diverse text: Include accented characters, CJK text, emoji, and right-to-left text in your test data. These will reveal encoding issues that ASCII text won't.

Conclusion

Character encodings are one of those fundamental topics that many developers never properly learn. We muddle through, fixing issues as they arise, without really understanding why they happen.

I hope this guide has given you a solid foundation. The key takeaways are:

  1. Character encodings map numbers to characters
  2. ASCII was the foundation, but it only covers English
  3. Unicode provides a universal character set
  4. UTF-8 is the encoding you should use in most cases
  5. Encoding issues arise when text is interpreted with the wrong encoding

When you encounter encoding problems, approach them systematically. Identify where the text came from, what encoding it should be, and where the mismatch occurred. With the knowledge from this guide, you'll be equipped to debug and fix these issues.

Remember, every piece of text on your computer - every file, every web page, every database entry - uses some encoding. Understanding that encoding is the key to ensuring your text displays correctly everywhere it goes.

If you have questions or want to share your own encoding war stories, feel free to reach out. Happy encoding!

Emma Taylor
Written byEmma TaylorDeveloper Advocate & Technical Writer
Read more articles