modify Java decoder in a way it could be transpiled to exception unfriendly languages

PiperOrigin-RevId: 769488037
diff --git a/java/org/brotli/dec/BitReader.java b/java/org/brotli/dec/BitReader.java
index f12099d..ec17f0c 100644
--- a/java/org/brotli/dec/BitReader.java
+++ b/java/org/brotli/dec/BitReader.java
@@ -6,6 +6,14 @@
 
 package org.brotli.dec;
 
+import static org.brotli.dec.BrotliError.BROTLI_ERROR;
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_CORRUPTED_PADDING_BITS;
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_READ_AFTER_END;
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_TRUNCATED_INPUT;
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_UNUSED_BYTES_AFTER_END;
+import static org.brotli.dec.BrotliError.BROTLI_OK;
+import static org.brotli.dec.BrotliError.BROTLI_PANIC_UNALIGNED_COPY_BYTES;
+
 /**
  * Bit reading helpers.
  */
@@ -16,8 +24,8 @@
   private static final int LOG_BITNESS = Utils.getLogBintness();
 
   // Not only Java compiler prunes "if (const false)" code, but JVM as well.
-  // Code under "if (DEBUG != 0)" have zero performance impact (outside unit tests).
-  private static final int DEBUG = Utils.isDebugMode();
+  // Code under "if (BIT_READER_DEBUG != 0)" have zero performance impact (outside unit tests).
+  private static final int BIT_READER_DEBUG = Utils.isDebugMode();
 
   static final int BITNESS = 1 << LOG_BITNESS;
 
@@ -36,30 +44,25 @@
   private static final int HALF_SIZE = BYTENESS / 2;
   private static final int HALVES_CAPACITY = CAPACITY / HALF_SIZE;
   private static final int HALF_BUFFER_SIZE = BUFFER_SIZE / HALF_SIZE;
-  private static final int HALF_WATERLINE = WATERLINE / HALF_SIZE;
 
   private static final int LOG_HALF_SIZE = LOG_BITNESS - 4;
 
+  static final int HALF_WATERLINE = WATERLINE / HALF_SIZE;
+
   /**
    * Fills up the input buffer.
    *
-   * <p> No-op if there are at least 36 bytes present after current position.
+   * <p> Should not be called if there are at least 36 bytes present after current position.
    *
    * <p> After encountering the end of the input stream, 64 additional zero bytes are copied to the
    * buffer.
    */
-  static void readMoreInput(State s) {
-    if (s.halfOffset > HALF_WATERLINE) {
-      doReadMoreInput(s);
-    }
-  }
-
-  static void doReadMoreInput(State s) {
+  static int readMoreInput(State s) {
     if (s.endOfStreamReached != 0) {
       if (halfAvailable(s) >= -2) {
-        return;
+        return BROTLI_OK;
       }
-      throw new BrotliRuntimeException("No more input");
+      return Utils.makeError(s, BROTLI_ERROR_TRUNCATED_INPUT);
     }
     final int readOffset = s.halfOffset << LOG_HALF_SIZE;
     int bytesInBuffer = CAPACITY - readOffset;
@@ -70,6 +73,9 @@
       final int spaceLeft = CAPACITY - bytesInBuffer;
       final int len = Utils.readInput(s, s.byteBuffer, bytesInBuffer, spaceLeft);
       // EOF is -1 in Java, but 0 in C#.
+      if (len < BROTLI_ERROR) {
+        return len;
+      }
       if (len <= 0) {
         s.endOfStreamReached = 1;
         s.tailBytes = bytesInBuffer;
@@ -79,19 +85,21 @@
       bytesInBuffer += len;
     }
     bytesToNibbles(s, bytesInBuffer);
+    return BROTLI_OK;
   }
 
-  static void checkHealth(State s, int endOfStream) {
+  static int checkHealth(State s, int endOfStream) {
     if (s.endOfStreamReached == 0) {
-      return;
+      return BROTLI_OK;
     }
     final int byteOffset = (s.halfOffset << LOG_HALF_SIZE) + ((s.bitOffset + 7) >> 3) - BYTENESS;
     if (byteOffset > s.tailBytes) {
-      throw new BrotliRuntimeException("Read after end");
+      return Utils.makeError(s, BROTLI_ERROR_READ_AFTER_END);
     }
     if ((endOfStream != 0) && (byteOffset != s.tailBytes)) {
-      throw new BrotliRuntimeException("Unused bytes after end");
+      return Utils.makeError(s, BROTLI_ERROR_UNUSED_BYTES_AFTER_END);
     }
+    return BROTLI_OK;
   }
 
   static void assertAccumulatorHealthy(State s) {
@@ -101,7 +109,7 @@
   }
 
   static void fillBitWindow(State s) {
-    if (DEBUG != 0) {
+    if (BIT_READER_DEBUG != 0) {
       assertAccumulatorHealthy(s);
     }
     if (s.bitOffset >= HALF_BITNESS) {
@@ -118,7 +126,7 @@
   }
 
   static void doFillBitWindow(State s) {
-    if (DEBUG != 0) {
+    if (BIT_READER_DEBUG != 0) {
       assertAccumulatorHealthy(s);
     }
     if (BITNESS == 64) {
@@ -165,7 +173,7 @@
     return low | (readFewBits(s, n - 16) << 16);
   }
 
-  static void initBitReader(State s) {
+  static int initBitReader(State s) {
     s.byteBuffer = new byte[BUFFER_SIZE];
     if (BITNESS == 64) {
       s.accumulator64 = 0;
@@ -177,30 +185,41 @@
     s.bitOffset = BITNESS;
     s.halfOffset = HALVES_CAPACITY;
     s.endOfStreamReached = 0;
-    prepare(s);
+    return prepare(s);
   }
 
-  private static void prepare(State s) {
-    readMoreInput(s);
-    checkHealth(s, 0);
-    doFillBitWindow(s);
-    doFillBitWindow(s);
-  }
-
-  static void reload(State s) {
-    if (s.bitOffset == BITNESS) {
-      prepare(s);
+  private static int prepare(State s) {
+    if (s.halfOffset > BitReader.HALF_WATERLINE) {
+      int result = readMoreInput(s);
+      if (result != BROTLI_OK) {
+        return result;
+      }
     }
+    int health = checkHealth(s, 0);
+    if (health != BROTLI_OK) {
+      return health;
+    }
+    doFillBitWindow(s);
+    doFillBitWindow(s);
+    return BROTLI_OK;
   }
 
-  static void jumpToByteBoundary(State s) {
+  static int reload(State s) {
+    if (s.bitOffset == BITNESS) {
+      return prepare(s);
+    }
+    return BROTLI_OK;
+  }
+
+  static int jumpToByteBoundary(State s) {
     final int padding = (BITNESS - s.bitOffset) & 7;
     if (padding != 0) {
       final int paddingBits = readFewBits(s, padding);
       if (paddingBits != 0) {
-        throw new BrotliRuntimeException("Corrupted padding bits");
+        return Utils.makeError(s, BROTLI_ERROR_CORRUPTED_PADDING_BITS);
       }
     }
+    return BROTLI_OK;
   }
 
   static int halfAvailable(State s) {
@@ -211,11 +230,11 @@
     return limit - s.halfOffset;
   }
 
-  static void copyRawBytes(State s, byte[] data, int offset, int length) {
+  static int copyRawBytes(State s, byte[] data, int offset, int length) {
     int pos = offset;
     int len = length;
     if ((s.bitOffset & 7) != 0) {
-      throw new BrotliRuntimeException("Unaligned copyBytes");
+      return Utils.makeError(s, BROTLI_PANIC_UNALIGNED_COPY_BYTES);
     }
 
     // Drain accumulator.
@@ -225,7 +244,7 @@
       len--;
     }
     if (len == 0) {
-      return;
+      return BROTLI_OK;
     }
 
     // Get data from shadow buffer with "sizeof(int)" granularity.
@@ -239,7 +258,7 @@
       s.halfOffset += copyNibbles;
     }
     if (len == 0) {
-      return;
+      return BROTLI_OK;
     }
 
     // Read tail bytes.
@@ -251,19 +270,23 @@
         s.bitOffset += 8;
         len--;
       }
-      checkHealth(s, 0);
-      return;
+      return checkHealth(s, 0);
     }
 
     // Now it is possible to copy bytes directly.
     while (len > 0) {
       final int chunkLen = Utils.readInput(s, data, pos, len);
-      if (chunkLen == -1) {
-        throw new BrotliRuntimeException("Unexpected end of input");
+      // EOF is -1 in Java, but 0 in C#.
+      if (len < BROTLI_ERROR) {
+        return len;
+      }
+      if (chunkLen <= 0) {
+        return Utils.makeError(s, BROTLI_ERROR_TRUNCATED_INPUT);
       }
       pos += chunkLen;
       len -= chunkLen;
     }
+    return BROTLI_OK;
   }
 
   /**
diff --git a/java/org/brotli/dec/BrotliError.java b/java/org/brotli/dec/BrotliError.java
new file mode 100644
index 0000000..223bbac
--- /dev/null
+++ b/java/org/brotli/dec/BrotliError.java
@@ -0,0 +1,47 @@
+/* Copyright 2025 Google Inc. All Rights Reserved.
+
+   Distributed under MIT license.
+   See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
+*/
+
+package org.brotli.dec;
+
+/**
+ * Possible errors from decoder.
+ */
+public class BrotliError {
+  public static final int BROTLI_OK = 0;
+  public static final int BROTLI_OK_DONE = BROTLI_OK + 1;
+  public static final int BROTLI_OK_NEED_MORE_OUTPUT = BROTLI_OK + 2;
+
+  // It is important that actual error codes are LESS than -1!
+  public static final int BROTLI_ERROR = -1;
+  public static final int BROTLI_ERROR_CORRUPTED_CODE_LENGTH_TABLE = BROTLI_ERROR - 1;
+  public static final int BROTLI_ERROR_CORRUPTED_CONTEXT_MAP = BROTLI_ERROR - 2;
+  public static final int BROTLI_ERROR_CORRUPTED_HUFFMAN_CODE_HISTOGRAM = BROTLI_ERROR - 3;
+  public static final int BROTLI_ERROR_CORRUPTED_PADDING_BITS = BROTLI_ERROR - 4;
+  public static final int BROTLI_ERROR_CORRUPTED_RESERVED_BIT = BROTLI_ERROR - 5;
+  public static final int BROTLI_ERROR_DUPLICATE_SIMPLE_HUFFMAN_SYMBOL = BROTLI_ERROR - 6;
+  public static final int BROTLI_ERROR_EXUBERANT_NIBBLE = BROTLI_ERROR - 7;
+  public static final int BROTLI_ERROR_INVALID_BACKWARD_REFERENCE = BROTLI_ERROR - 8;
+  public static final int BROTLI_ERROR_INVALID_METABLOCK_LENGTH = BROTLI_ERROR - 9;
+  public static final int BROTLI_ERROR_INVALID_WINDOW_BITS = BROTLI_ERROR - 10;
+  public static final int BROTLI_ERROR_NEGATIVE_DISTANCE = BROTLI_ERROR - 11;
+  public static final int BROTLI_ERROR_READ_AFTER_END = BROTLI_ERROR - 12;
+  public static final int BROTLI_ERROR_READ_FAILED = BROTLI_ERROR - 13;
+  public static final int BROTLI_ERROR_SYMBOL_OUT_OF_RANGE = BROTLI_ERROR - 14;
+  public static final int BROTLI_ERROR_TRUNCATED_INPUT = BROTLI_ERROR - 15;
+  public static final int BROTLI_ERROR_UNUSED_BYTES_AFTER_END = BROTLI_ERROR - 16;
+  public static final int BROTLI_ERROR_UNUSED_HUFFMAN_SPACE = BROTLI_ERROR - 17;
+
+  public static final int BROTLI_PANIC = -21;
+  public static final int BROTLI_PANIC_ALREADY_CLOSED = BROTLI_PANIC - 1;
+  public static final int BROTLI_PANIC_MAX_DISTANCE_TOO_SMALL = BROTLI_PANIC - 2;
+  public static final int BROTLI_PANIC_STATE_NOT_FRESH = BROTLI_PANIC - 3;
+  public static final int BROTLI_PANIC_STATE_NOT_INITIALIZED = BROTLI_PANIC - 4;
+  public static final int BROTLI_PANIC_STATE_NOT_UNINITIALIZED = BROTLI_PANIC - 5;
+  public static final int BROTLI_PANIC_TOO_MANY_DICTIONARY_CHUNKS = BROTLI_PANIC - 6;
+  public static final int BROTLI_PANIC_UNEXPECTED_STATE = BROTLI_PANIC - 7;
+  public static final int BROTLI_PANIC_UNREACHABLE = BROTLI_PANIC - 8;
+  public static final int BROTLI_PANIC_UNALIGNED_COPY_BYTES = BROTLI_PANIC - 9;
+}
diff --git a/java/org/brotli/dec/BrotliInputStream.java b/java/org/brotli/dec/BrotliInputStream.java
index 46df98f..24935db 100644
--- a/java/org/brotli/dec/BrotliInputStream.java
+++ b/java/org/brotli/dec/BrotliInputStream.java
@@ -111,6 +111,7 @@
   @Override
   public void close() throws IOException {
     Decode.close(state);
+    Utils.closeInput(state);
   }
 
   /**
diff --git a/java/org/brotli/dec/Decode.java b/java/org/brotli/dec/Decode.java
index f5df240..345e9cd 100644
--- a/java/org/brotli/dec/Decode.java
+++ b/java/org/brotli/dec/Decode.java
@@ -6,7 +6,30 @@
 
 package org.brotli.dec;
 
-import java.io.IOException;
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_CORRUPTED_CODE_LENGTH_TABLE;
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_CORRUPTED_CONTEXT_MAP;
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_CORRUPTED_HUFFMAN_CODE_HISTOGRAM;
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_CORRUPTED_RESERVED_BIT;
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_DUPLICATE_SIMPLE_HUFFMAN_SYMBOL;
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_EXUBERANT_NIBBLE;
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_INVALID_BACKWARD_REFERENCE;
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_INVALID_METABLOCK_LENGTH;
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_INVALID_WINDOW_BITS;
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_NEGATIVE_DISTANCE;
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_SYMBOL_OUT_OF_RANGE;
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_UNUSED_HUFFMAN_SPACE;
+import static org.brotli.dec.BrotliError.BROTLI_OK;
+import static org.brotli.dec.BrotliError.BROTLI_OK_DONE;
+import static org.brotli.dec.BrotliError.BROTLI_OK_NEED_MORE_OUTPUT;
+import static org.brotli.dec.BrotliError.BROTLI_PANIC_ALREADY_CLOSED;
+import static org.brotli.dec.BrotliError.BROTLI_PANIC_MAX_DISTANCE_TOO_SMALL;
+import static org.brotli.dec.BrotliError.BROTLI_PANIC_STATE_NOT_FRESH;
+import static org.brotli.dec.BrotliError.BROTLI_PANIC_STATE_NOT_INITIALIZED;
+import static org.brotli.dec.BrotliError.BROTLI_PANIC_STATE_NOT_UNINITIALIZED;
+import static org.brotli.dec.BrotliError.BROTLI_PANIC_TOO_MANY_DICTIONARY_CHUNKS;
+import static org.brotli.dec.BrotliError.BROTLI_PANIC_UNEXPECTED_STATE;
+import static org.brotli.dec.BrotliError.BROTLI_PANIC_UNREACHABLE;
+
 import java.nio.ByteBuffer;
 
 /**
@@ -22,6 +45,7 @@
   //----------------------------------------------------------------------------
   // RunningState
   //----------------------------------------------------------------------------
+  // NB: negative values are used for errors.
   private static final int UNINITIALIZED = 0;
   private static final int INITIALIZED = 1;
   private static final int BLOCK_START = 2;
@@ -156,9 +180,9 @@
 
   // TODO(eustas): add a correctness test for this function when
   //               large-window and dictionary are implemented.
-  private static int calculateDistanceAlphabetLimit(int maxDistance, int npostfix, int ndirect) {
+  private static int calculateDistanceAlphabetLimit(State s, int maxDistance, int npostfix, int ndirect) {
     if (maxDistance < ndirect + (2 << npostfix)) {
-      throw new IllegalArgumentException("maxDistance is too small");
+      return Utils.makeError(s, BROTLI_PANIC_MAX_DISTANCE_TOO_SMALL);
     }
     final int offset = ((maxDistance - ndirect) >> npostfix) + 4;
     final int ndistbits = log2floor(offset) - 1;
@@ -237,9 +261,8 @@
           return -1;
         }
         return n;
-      } else {
-        return 8 + n;
       }
+      return 8 + n;
     }
     return 17;
   }
@@ -251,24 +274,26 @@
    *
    * @param s initialized state, before any read is performed.
    */
-  static void enableEagerOutput(State s) {
+  static int enableEagerOutput(State s) {
     if (s.runningState != INITIALIZED) {
-      throw new IllegalStateException("State MUST be freshly initialized");
+      return Utils.makeError(s, BROTLI_PANIC_STATE_NOT_FRESH);
     }
     s.isEager = 1;
+    return BROTLI_OK;
   }
 
-  static void enableLargeWindow(State s) {
+  static int enableLargeWindow(State s) {
     if (s.runningState != INITIALIZED) {
-      throw new IllegalStateException("State MUST be freshly initialized");
+      return Utils.makeError(s, BROTLI_PANIC_STATE_NOT_FRESH);
     }
     s.isLargeWindow = 1;
+    return BROTLI_OK;
   }
 
   // TODO(eustas): do we need byte views?
-  static void attachDictionaryChunk(State s, byte[] data) {
+  static int attachDictionaryChunk(State s, byte[] data) {
     if (s.runningState != INITIALIZED) {
-      throw new IllegalStateException("State MUST be freshly initialized");
+      return Utils.makeError(s, BROTLI_PANIC_STATE_NOT_FRESH);
     }
     if (s.cdNumChunks == 0) {
       s.cdChunks = new byte[16][];
@@ -276,45 +301,52 @@
       s.cdBlockBits = -1;
     }
     if (s.cdNumChunks == 15) {
-      throw new IllegalStateException("Too many dictionary chunks");
+      return Utils.makeError(s, BROTLI_PANIC_TOO_MANY_DICTIONARY_CHUNKS);
     }
     s.cdChunks[s.cdNumChunks] = data;
     s.cdNumChunks++;
     s.cdTotalSize += data.length;
     s.cdChunkOffsets[s.cdNumChunks] = s.cdTotalSize;
+    return BROTLI_OK;
   }
 
   /**
    * Associate input with decoder state.
    *
    * @param s uninitialized state without associated input
-   * @param input compressed data source
    */
-  static void initState(State s) {
+  static int initState(State s) {
     if (s.runningState != UNINITIALIZED) {
-      throw new IllegalStateException("State MUST be uninitialized");
+      return Utils.makeError(s, BROTLI_PANIC_STATE_NOT_UNINITIALIZED);
     }
     /* 6 trees + 1 extra "offset" slot to simplify table decoding logic. */
     s.blockTrees = new int[7 + 3 * (HUFFMAN_TABLE_SIZE_258 + HUFFMAN_TABLE_SIZE_26)];
     s.blockTrees[0] = 7;
     s.distRbIdx = 3;
-    final int maxDistanceAlphabetLimit =
-        calculateDistanceAlphabetLimit(MAX_ALLOWED_DISTANCE, 3, 15 << 3);
+    int result = calculateDistanceAlphabetLimit(s, MAX_ALLOWED_DISTANCE, 3, 15 << 3);
+    if (result < BROTLI_OK) {
+      return result;
+    }
+    final int maxDistanceAlphabetLimit = result;
     s.distExtraBits = new byte[maxDistanceAlphabetLimit];
     s.distOffset = new int[maxDistanceAlphabetLimit];
-    BitReader.initBitReader(s);
+    result = BitReader.initBitReader(s);
+    if (result < BROTLI_OK) {
+      return result;
+    }
     s.runningState = INITIALIZED;
+    return BROTLI_OK;
   }
 
-  static void close(State s) throws IOException {
+  static int close(State s) {
     if (s.runningState == UNINITIALIZED) {
-      throw new IllegalStateException("State MUST be initialized");
+      return Utils.makeError(s, BROTLI_PANIC_STATE_NOT_INITIALIZED);
     }
     if (s.runningState == CLOSED) {
-      return;
+      return Utils.makeError(s, BROTLI_PANIC_ALREADY_CLOSED);
     }
     s.runningState = CLOSED;
-    Utils.closeInput(s);
+    return BROTLI_OK;
   }
 
   /**
@@ -326,37 +358,36 @@
       final int n = BitReader.readFewBits(s, 3);
       if (n == 0) {
         return 1;
-      } else {
-        return BitReader.readFewBits(s, n) + (1 << n);
       }
+      return BitReader.readFewBits(s, n) + (1 << n);
     }
     return 0;
   }
 
-  private static void decodeMetaBlockLength(State s) {
+  private static int decodeMetaBlockLength(State s) {
     BitReader.fillBitWindow(s);
     s.inputEnd = BitReader.readFewBits(s, 1);
     s.metaBlockLength = 0;
     s.isUncompressed = 0;
     s.isMetadata = 0;
     if ((s.inputEnd != 0) && BitReader.readFewBits(s, 1) != 0) {
-      return;
+      return BROTLI_OK;
     }
     final int sizeNibbles = BitReader.readFewBits(s, 2) + 4;
     if (sizeNibbles == 7) {
       s.isMetadata = 1;
       if (BitReader.readFewBits(s, 1) != 0) {
-        throw new BrotliRuntimeException("Corrupted reserved bit");
+        return Utils.makeError(s, BROTLI_ERROR_CORRUPTED_RESERVED_BIT);
       }
       final int sizeBytes = BitReader.readFewBits(s, 2);
       if (sizeBytes == 0) {
-        return;
+        return BROTLI_OK;
       }
       for (int i = 0; i < sizeBytes; ++i) {
         BitReader.fillBitWindow(s);
         final int bits = BitReader.readFewBits(s, 8);
         if (bits == 0 && i + 1 == sizeBytes && sizeBytes > 1) {
-          throw new BrotliRuntimeException("Exuberant nibble");
+          return Utils.makeError(s, BROTLI_ERROR_EXUBERANT_NIBBLE);
         }
         s.metaBlockLength += bits << (i * 8);
       }
@@ -365,7 +396,7 @@
         BitReader.fillBitWindow(s);
         final int bits = BitReader.readFewBits(s, 4);
         if (bits == 0 && i + 1 == sizeNibbles && sizeNibbles > 4) {
-          throw new BrotliRuntimeException("Exuberant nibble");
+          return Utils.makeError(s, BROTLI_ERROR_EXUBERANT_NIBBLE);
         }
         s.metaBlockLength += bits << (i * 4);
       }
@@ -374,6 +405,7 @@
     if (s.inputEnd == 0) {
       s.isUncompressed = BitReader.readFewBits(s, 1);
     }
+    return BROTLI_OK;
   }
 
   /**
@@ -428,7 +460,7 @@
     }
   }
 
-  private static void readHuffmanCodeLengths(
+  private static int readHuffmanCodeLengths(
       int[] codeLengthCodeLengths, int numSymbols, int[] codeLengths, State s) {
     int symbol = 0;
     int prevCodeLen = DEFAULT_CODE_LENGTH;
@@ -440,7 +472,12 @@
     Huffman.buildHuffmanTable(table, tableIdx, 5, codeLengthCodeLengths, CODE_LENGTH_CODES);
 
     while (symbol < numSymbols && space > 0) {
-      BitReader.readMoreInput(s);
+      if (s.halfOffset > BitReader.HALF_WATERLINE) {
+        int result = BitReader.readMoreInput(s);
+        if (result < BROTLI_OK) {
+          return result;
+        }
+      }
       BitReader.fillBitWindow(s);
       final int p = BitReader.peekBits(s) & 31;
       s.bitOffset += table[p] >> 16;
@@ -471,7 +508,7 @@
         repeat += BitReader.readFewBits(s, extraBits) + 3;
         final int repeatDelta = repeat - oldRepeat;
         if (symbol + repeatDelta > numSymbols) {
-          throw new BrotliRuntimeException("symbol + repeatDelta > numSymbols"); // COV_NF_LINE
+          return Utils.makeError(s, BROTLI_ERROR_CORRUPTED_CODE_LENGTH_TABLE);
         }
         for (int i = 0; i < repeatDelta; ++i) {
           codeLengths[symbol++] = repeatCodeLen;
@@ -482,20 +519,22 @@
       }
     }
     if (space != 0) {
-      throw new BrotliRuntimeException("Unused space"); // COV_NF_LINE
+      return Utils.makeError(s, BROTLI_ERROR_UNUSED_HUFFMAN_SPACE);
     }
     // TODO(eustas): Pass max_symbol to Huffman table builder instead?
     Utils.fillIntsWithZeroes(codeLengths, symbol, numSymbols);
+    return BROTLI_OK;
   }
 
-  private static void checkDupes(int[] symbols, int length) {
+  private static int checkDupes(State s, int[] symbols, int length) {
     for (int i = 0; i < length - 1; ++i) {
       for (int j = i + 1; j < length; ++j) {
         if (symbols[i] == symbols[j]) {
-          throw new BrotliRuntimeException("Duplicate simple Huffman code symbol"); // COV_NF_LINE
+          return Utils.makeError(s, BROTLI_ERROR_DUPLICATE_SIMPLE_HUFFMAN_SYMBOL);
         }
       }
     }
+    return BROTLI_OK;
   }
 
   /**
@@ -514,11 +553,14 @@
       BitReader.fillBitWindow(s);
       final int symbol = BitReader.readFewBits(s, maxBits);
       if (symbol >= alphabetSizeLimit) {
-        throw new BrotliRuntimeException("Can't readHuffmanCode"); // COV_NF_LINE
+        return Utils.makeError(s, BROTLI_ERROR_SYMBOL_OUT_OF_RANGE);
       }
       symbols[i] = symbol;
     }
-    checkDupes(symbols, numSymbols);
+    int result = checkDupes(s, symbols, numSymbols);
+    if (result < BROTLI_OK) {
+      return result;
+    }
 
     int histogramId = numSymbols;
     if (numSymbols == 4) {
@@ -583,14 +625,19 @@
       if (v != 0) {
         space -= (32 >> v);
         numCodes++;
-        if (space <= 0) break;
+        if (space <= 0) {
+          break;
+        }
       }
     }
     if (space != 0 && numCodes != 1) {
-      throw new BrotliRuntimeException("Corrupted Huffman code histogram"); // COV_NF_LINE
+      return Utils.makeError(s, BROTLI_ERROR_CORRUPTED_HUFFMAN_CODE_HISTOGRAM);
     }
 
-    readHuffmanCodeLengths(codeLengthCodeLengths, alphabetSizeLimit, codeLengths, s);
+    int result = readHuffmanCodeLengths(codeLengthCodeLengths, alphabetSizeLimit, codeLengths, s);
+    if (result < BROTLI_OK) {
+      return result;
+    }
 
     return Huffman.buildHuffmanTable(
         tableGroup, tableIdx, HUFFMAN_TABLE_BITS, codeLengths, alphabetSizeLimit);
@@ -603,18 +650,28 @@
    */
   private static int readHuffmanCode(int alphabetSizeMax, int alphabetSizeLimit,
       int[] tableGroup, int tableIdx, State s) {
-    BitReader.readMoreInput(s);
+    if (s.halfOffset > BitReader.HALF_WATERLINE) {
+      int result = BitReader.readMoreInput(s);
+      if (result < BROTLI_OK) {
+        return result;
+      }
+    }
     BitReader.fillBitWindow(s);
     final int simpleCodeOrSkip = BitReader.readFewBits(s, 2);
     if (simpleCodeOrSkip == 1) {
       return readSimpleHuffmanCode(alphabetSizeMax, alphabetSizeLimit, tableGroup, tableIdx, s);
-    } else {
-      return readComplexHuffmanCode(alphabetSizeLimit, simpleCodeOrSkip, tableGroup, tableIdx, s);
     }
+    return readComplexHuffmanCode(alphabetSizeLimit, simpleCodeOrSkip, tableGroup, tableIdx, s);
   }
 
   private static int decodeContextMap(int contextMapSize, byte[] contextMap, State s) {
-    BitReader.readMoreInput(s);
+    int result;
+    if (s.halfOffset > BitReader.HALF_WATERLINE) {
+      result = BitReader.readMoreInput(s);
+      if (result < BROTLI_OK) {
+        return result;
+      }
+    }
     final int numTrees = decodeVarLenUnsignedByte(s) + 1;
 
     if (numTrees == 1) {
@@ -633,10 +690,18 @@
     /* Speculative single entry table group. */
     final int[] table = new int[tableSize + 1];
     final int tableIdx = table.length - 1;
-    readHuffmanCode(alphabetSize, alphabetSize, table, tableIdx, s);
+    result = readHuffmanCode(alphabetSize, alphabetSize, table, tableIdx, s);
+    if (result < BROTLI_OK) {
+      return result;
+    }
     int i = 0;
     while (i < contextMapSize) {
-      BitReader.readMoreInput(s);
+      if (s.halfOffset > BitReader.HALF_WATERLINE) {
+        result = BitReader.readMoreInput(s);
+        if (result < BROTLI_OK) {
+          return result;
+        }
+      }
       BitReader.fillBitWindow(s);
       final int code = readSymbol(table, tableIdx, s);
       if (code == 0) {
@@ -647,7 +712,7 @@
         int reps = (1 << code) + BitReader.readFewBits(s, code);
         while (reps != 0) {
           if (i >= contextMapSize) {
-            throw new BrotliRuntimeException("Corrupted context map"); // COV_NF_LINE
+            return Utils.makeError(s, BROTLI_ERROR_CORRUPTED_CONTEXT_MAP);
           }
           contextMap[i] = 0;
           i++;
@@ -732,24 +797,36 @@
     s.ringBufferSize = newSize;
   }
 
-  private static void readNextMetablockHeader(State s) {
+  private static int readNextMetablockHeader(State s) {
     if (s.inputEnd != 0) {
       s.nextRunningState = FINISHED;
       s.runningState = INIT_WRITE;
-      return;
+      return BROTLI_OK;
     }
     // TODO(eustas): Reset? Do we need this?
     s.literalTreeGroup = new int[0];
     s.commandTreeGroup = new int[0];
     s.distanceTreeGroup = new int[0];
 
-    BitReader.readMoreInput(s);
-    decodeMetaBlockLength(s);
+    int result;
+    if (s.halfOffset > BitReader.HALF_WATERLINE) {
+      result = BitReader.readMoreInput(s);
+      if (result < BROTLI_OK) {
+        return result;
+      }
+    }
+    result = decodeMetaBlockLength(s);
+    if (result < BROTLI_OK) {
+      return result;
+    }
     if ((s.metaBlockLength == 0) && (s.isMetadata == 0)) {
-      return;
+      return BROTLI_OK;
     }
     if ((s.isUncompressed != 0) || (s.isMetadata != 0)) {
-      BitReader.jumpToByteBoundary(s);
+      result = BitReader.jumpToByteBoundary(s);
+      if (result < BROTLI_OK) {
+        return result;
+      }
       if (s.isMetadata == 0) {
         s.runningState = COPY_UNCOMPRESSED;
       } else {
@@ -760,7 +837,7 @@
     }
 
     if (s.isMetadata != 0) {
-      return;
+      return BROTLI_OK;
     }
     s.expectedTotalSize += s.metaBlockLength;
     if (s.expectedTotalSize > 1 << 30) {
@@ -769,6 +846,7 @@
     if (s.ringBufferSize < s.maxRingBufferSize) {
       maybeReallocateRingBuffer(s);
     }
+    return BROTLI_OK;
   }
 
   private static int readMetablockPartition(State s, int treeType, int numBlockTypes) {
@@ -780,13 +858,21 @@
     }
 
     final int blockTypeAlphabetSize = numBlockTypes + 2;
-    offset += readHuffmanCode(
+    int result = readHuffmanCode(
         blockTypeAlphabetSize, blockTypeAlphabetSize, s.blockTrees, 2 * treeType, s);
+    if (result < BROTLI_OK) {
+      return result;
+    }
+    offset += result;
     s.blockTrees[2 * treeType + 1] = offset;
 
     final int blockLengthAlphabetSize = NUM_BLOCK_LENGTH_CODES;
-    offset += readHuffmanCode(
+    result = readHuffmanCode(
         blockLengthAlphabetSize, blockLengthAlphabetSize, s.blockTrees, 2 * treeType + 1, s);
+    if (result < BROTLI_OK) {
+      return result;
+    }
+    offset += result;
     s.blockTrees[2 * treeType + 2] = offset;
 
     return readBlockLength(s.blockTrees, 2 * treeType + 1, s);
@@ -825,15 +911,32 @@
     }
   }
 
-  private static void readMetablockHuffmanCodesAndContextMaps(State s) {
+  private static int readMetablockHuffmanCodesAndContextMaps(State s) {
     s.numLiteralBlockTypes = decodeVarLenUnsignedByte(s) + 1;
-    s.literalBlockLength = readMetablockPartition(s, 0, s.numLiteralBlockTypes);
+    int result = readMetablockPartition(s, 0, s.numLiteralBlockTypes);
+    if (result < BROTLI_OK) {
+      return result;
+    }
+    s.literalBlockLength = result;
     s.numCommandBlockTypes = decodeVarLenUnsignedByte(s) + 1;
-    s.commandBlockLength = readMetablockPartition(s, 1, s.numCommandBlockTypes);
+    result = readMetablockPartition(s, 1, s.numCommandBlockTypes);
+    if (result < BROTLI_OK) {
+      return result;
+    }
+    s.commandBlockLength = result;
     s.numDistanceBlockTypes = decodeVarLenUnsignedByte(s) + 1;
-    s.distanceBlockLength = readMetablockPartition(s, 2, s.numDistanceBlockTypes);
+    result = readMetablockPartition(s, 2, s.numDistanceBlockTypes);
+    if (result < BROTLI_OK) {
+      return result;
+    }
+    s.distanceBlockLength = result;
 
-    BitReader.readMoreInput(s);
+    if (s.halfOffset > BitReader.HALF_WATERLINE) {
+      result = BitReader.readMoreInput(s);
+      if (result < BROTLI_OK) {
+        return result;
+      }
+    }
     BitReader.fillBitWindow(s);
     s.distancePostfixBits = BitReader.readFewBits(s, 2);
     s.numDirectDistanceCodes = BitReader.readFewBits(s, 4) << s.distancePostfixBits;
@@ -848,13 +951,22 @@
         s.contextModes[i] = (byte) BitReader.readFewBits(s, 2);
         i++;
       }
-      BitReader.readMoreInput(s);
+      if (s.halfOffset > BitReader.HALF_WATERLINE) {
+        result = BitReader.readMoreInput(s);
+        if (result < BROTLI_OK) {
+          return result;
+        }
+      }
     }
 
     // TODO(eustas): Reuse?
     final int contextMapLength = s.numLiteralBlockTypes << LITERAL_CONTEXT_BITS;
     s.contextMap = new byte[contextMapLength];
-    final int numLiteralTrees = decodeContextMap(contextMapLength, s.contextMap, s);
+    result = decodeContextMap(contextMapLength, s.contextMap, s);
+    if (result < BROTLI_OK) {
+      return result;
+    }
+    final int numLiteralTrees = result;
     s.trivialLiteralContext = 1;
     for (int j = 0; j < contextMapLength; ++j) {
       if ((int) s.contextMap[j] != j >> LITERAL_CONTEXT_BITS) {
@@ -865,24 +977,46 @@
 
     // TODO(eustas): Reuse?
     s.distContextMap = new byte[s.numDistanceBlockTypes << DISTANCE_CONTEXT_BITS];
-    final int numDistTrees = decodeContextMap(s.numDistanceBlockTypes << DISTANCE_CONTEXT_BITS,
+    result = decodeContextMap(s.numDistanceBlockTypes << DISTANCE_CONTEXT_BITS,
         s.distContextMap, s);
+    if (result < BROTLI_OK) {
+      return result;
+    }
+    final int numDistTrees = result;
 
-    s.literalTreeGroup = decodeHuffmanTreeGroup(NUM_LITERAL_CODES, NUM_LITERAL_CODES,
-        numLiteralTrees, s);
-    s.commandTreeGroup = decodeHuffmanTreeGroup(NUM_COMMAND_CODES, NUM_COMMAND_CODES,
-        s.numCommandBlockTypes, s);
+    s.literalTreeGroup = new int[huffmanTreeGroupAllocSize(NUM_LITERAL_CODES, numLiteralTrees)];
+    result = decodeHuffmanTreeGroup(
+        NUM_LITERAL_CODES, NUM_LITERAL_CODES, numLiteralTrees, s, s.literalTreeGroup);
+    if (result < BROTLI_OK) {
+      return result;
+    }
+    s.commandTreeGroup =
+        new int[huffmanTreeGroupAllocSize(NUM_COMMAND_CODES, s.numCommandBlockTypes)];
+    result = decodeHuffmanTreeGroup(
+        NUM_COMMAND_CODES, NUM_COMMAND_CODES, s.numCommandBlockTypes, s, s.commandTreeGroup);
+    if (result < BROTLI_OK) {
+      return result;
+    }
     int distanceAlphabetSizeMax = calculateDistanceAlphabetSize(
         s.distancePostfixBits, s.numDirectDistanceCodes, MAX_DISTANCE_BITS);
     int distanceAlphabetSizeLimit = distanceAlphabetSizeMax;
     if (s.isLargeWindow == 1) {
       distanceAlphabetSizeMax = calculateDistanceAlphabetSize(
           s.distancePostfixBits, s.numDirectDistanceCodes, MAX_LARGE_WINDOW_DISTANCE_BITS);
-      distanceAlphabetSizeLimit = calculateDistanceAlphabetLimit(
-          MAX_ALLOWED_DISTANCE, s.distancePostfixBits, s.numDirectDistanceCodes);
+      result = calculateDistanceAlphabetLimit(
+          s, MAX_ALLOWED_DISTANCE, s.distancePostfixBits, s.numDirectDistanceCodes);
+      if (result < BROTLI_OK) {
+        return result;
+      }
+      distanceAlphabetSizeLimit = result;
     }
-    s.distanceTreeGroup = decodeHuffmanTreeGroup(distanceAlphabetSizeMax, distanceAlphabetSizeLimit,
-        numDistTrees, s);
+    s.distanceTreeGroup =
+        new int[huffmanTreeGroupAllocSize(distanceAlphabetSizeLimit, numDistTrees)];
+    result = decodeHuffmanTreeGroup(
+        distanceAlphabetSizeMax, distanceAlphabetSizeLimit, numDistTrees, s, s.distanceTreeGroup);
+    if (result < BROTLI_OK) {
+      return result;
+    }
     calculateDistanceLut(s, distanceAlphabetSizeLimit);
 
     s.contextMapSlice = 0;
@@ -898,30 +1032,42 @@
     s.rings[7] = 0;
     s.rings[8] = 1;
     s.rings[9] = 0;
+    return BROTLI_OK;
   }
 
-  private static void copyUncompressedData(State s) {
+  private static int copyUncompressedData(State s) {
     final byte[] ringBuffer = s.ringBuffer;
+    int result;
 
     // Could happen if block ends at ring buffer end.
     if (s.metaBlockLength <= 0) {
-      BitReader.reload(s);
+      result = BitReader.reload(s);
+      if (result < BROTLI_OK) {
+        return result;
+      }
       s.runningState = BLOCK_START;
-      return;
+      return BROTLI_OK;
     }
 
     final int chunkLength = Utils.min(s.ringBufferSize - s.pos, s.metaBlockLength);
-    BitReader.copyRawBytes(s, ringBuffer, s.pos, chunkLength);
+    result = BitReader.copyRawBytes(s, ringBuffer, s.pos, chunkLength);
+    if (result < BROTLI_OK) {
+      return result;
+    }
     s.metaBlockLength -= chunkLength;
     s.pos += chunkLength;
     if (s.pos == s.ringBufferSize) {
         s.nextRunningState = COPY_UNCOMPRESSED;
         s.runningState = INIT_WRITE;
-        return;
+        return BROTLI_OK;
       }
 
-    BitReader.reload(s);
+    result = BitReader.reload(s);
+    if (result < BROTLI_OK) {
+      return result;
+    }
     s.runningState = BLOCK_START;
+    return BROTLI_OK;
   }
 
   private static int writeRingBuffer(State s) {
@@ -936,22 +1082,28 @@
     }
 
     if (s.outputUsed < s.outputLength) {
-      return 1;
-    } else {
-      return 0;
+      return BROTLI_OK;
     }
+    return BROTLI_OK_NEED_MORE_OUTPUT;
   }
 
-  private static int[] decodeHuffmanTreeGroup(int alphabetSizeMax, int alphabetSizeLimit,
-      int n, State s) {
+  private static int huffmanTreeGroupAllocSize(int alphabetSizeLimit, int n) {
     final int maxTableSize = MAX_HUFFMAN_TABLE_SIZE[(alphabetSizeLimit + 31) >> 5];
-    final int[] group = new int[n + n * maxTableSize];
+    return n + n * maxTableSize;
+  }
+
+  private static int decodeHuffmanTreeGroup(int alphabetSizeMax, int alphabetSizeLimit,
+      int n, State s, int[] group) {
     int next = n;
     for (int i = 0; i < n; ++i) {
       group[i] = next;
-      next += readHuffmanCode(alphabetSizeMax, alphabetSizeLimit, group, i, s);
+      int result = readHuffmanCode(alphabetSizeMax, alphabetSizeLimit, group, i, s);
+      if (result < BROTLI_OK) {
+        return result;
+      }
+      next += result;
     }
-    return group;
+    return BROTLI_OK;
   }
 
   // Returns offset in ringBuffer that should trigger WRITE when filled.
@@ -963,24 +1115,27 @@
     return result;
   }
 
-  private static void doUseDictionary(State s, int fence) {
+  private static int doUseDictionary(State s, int fence) {
     if (s.distance > MAX_ALLOWED_DISTANCE) {
-      throw new BrotliRuntimeException("Invalid backward reference");
+      return Utils.makeError(s, BROTLI_ERROR_INVALID_BACKWARD_REFERENCE);
     }
     final int address = s.distance - s.maxDistance - 1 - s.cdTotalSize;
     if (address < 0) {
-      initializeCompoundDictionaryCopy(s, -address - 1, s.copyLength);
+      int result = initializeCompoundDictionaryCopy(s, -address - 1, s.copyLength);
+      if (result < BROTLI_OK) {
+        return result;
+      }
       s.runningState = COPY_FROM_COMPOUND_DICTIONARY;
     } else {
       // Force lazy dictionary initialization.
       final ByteBuffer dictionaryData = Dictionary.getData();
       final int wordLength = s.copyLength;
       if (wordLength > Dictionary.MAX_DICTIONARY_WORD_LENGTH) {
-        throw new BrotliRuntimeException("Invalid backward reference"); // COV_NF_LINE
+        return Utils.makeError(s, BROTLI_ERROR_INVALID_BACKWARD_REFERENCE);
       }
       final int shift = Dictionary.sizeBits[wordLength];
       if (shift == 0) {
-        throw new BrotliRuntimeException("Invalid backward reference"); // COV_NF_LINE
+        return Utils.makeError(s, BROTLI_ERROR_INVALID_BACKWARD_REFERENCE);
       }
       int offset = Dictionary.offsets[wordLength];
       final int mask = (1 << shift) - 1;
@@ -989,7 +1144,7 @@
       offset += wordIdx * wordLength;
       final Transform.Transforms transforms = Transform.RFC_TRANSFORMS;
       if (transformIdx >= transforms.numTransforms) {
-        throw new BrotliRuntimeException("Invalid backward reference"); // COV_NF_LINE
+        return Utils.makeError(s, BROTLI_ERROR_INVALID_BACKWARD_REFERENCE);
       }
       final int len = Transform.transformDictionaryWord(s.ringBuffer, s.pos, dictionaryData,
           offset, wordLength, transforms, transformIdx);
@@ -998,10 +1153,11 @@
       if (s.pos >= fence) {
         s.nextRunningState = MAIN_LOOP;
         s.runningState = INIT_WRITE;
-        return;
+        return BROTLI_OK;
       }
       s.runningState = MAIN_LOOP;
     }
+    return BROTLI_OK;
   }
 
   private static void initializeCompoundDictionary(State s) {
@@ -1024,7 +1180,7 @@
     }
   }
 
-  private static void initializeCompoundDictionaryCopy(State s, int address, int length) {
+  private static int initializeCompoundDictionaryCopy(State s, int address, int length) {
     if (s.cdBlockBits == -1) {
       initializeCompoundDictionary(s);
     }
@@ -1033,7 +1189,7 @@
       index++;
     }
     if (s.cdTotalSize > address + length) {
-      throw new BrotliRuntimeException("Invalid backward reference");
+      return Utils.makeError(s, BROTLI_ERROR_INVALID_BACKWARD_REFERENCE);
     }
     /* Update the recent distances cache */
     s.distRbIdx = (s.distRbIdx + 1) & 0x3;
@@ -1043,6 +1199,7 @@
     s.cdBrOffset = address - s.cdChunkOffsets[index];
     s.cdBrLength = length;
     s.cdBrCopied = 0;
+    return BROTLI_OK;
   }
 
   private static int copyFromCompoundDictionary(State s, int fence) {
@@ -1078,17 +1235,21 @@
   /**
    * Actual decompress implementation.
    */
-  static void decompress(State s) {
+  static int decompress(State s) {
+    int result;
     if (s.runningState == UNINITIALIZED) {
-      throw new IllegalStateException("Can't decompress until initialized");
+      return Utils.makeError(s, BROTLI_PANIC_STATE_NOT_INITIALIZED);
+    }
+    if (s.runningState < 0) {
+      return Utils.makeError(s, BROTLI_PANIC_UNEXPECTED_STATE);
     }
     if (s.runningState == CLOSED) {
-      throw new IllegalStateException("Can't decompress after close");
+      return Utils.makeError(s, BROTLI_PANIC_ALREADY_CLOSED);
     }
     if (s.runningState == INITIALIZED) {
       final int windowBits = decodeWindowBits(s);
       if (windowBits == -1) {  /* Reserved case for future expansion. */
-        throw new BrotliRuntimeException("Invalid 'windowBits' code");
+        return Utils.makeError(s, BROTLI_ERROR_INVALID_WINDOW_BITS);
       }
       s.maxRingBufferSize = 1 << windowBits;
       s.maxBackwardDistance = s.maxRingBufferSize - 16;
@@ -1104,26 +1265,38 @@
       switch (s.runningState) {
         case BLOCK_START:
           if (s.metaBlockLength < 0) {
-            throw new BrotliRuntimeException("Invalid metablock length");
+            return Utils.makeError(s, BROTLI_ERROR_INVALID_METABLOCK_LENGTH);
           }
-          readNextMetablockHeader(s);
+          result = readNextMetablockHeader(s);
+          if (result < BROTLI_OK) {
+            return result;
+          }
           /* Ring-buffer would be reallocated here. */
           fence = calculateFence(s);
           ringBufferMask = s.ringBufferSize - 1;
           ringBuffer = s.ringBuffer;
           continue;
 
-        case COMPRESSED_BLOCK_START:
-          readMetablockHuffmanCodesAndContextMaps(s);
+        case COMPRESSED_BLOCK_START: {
+          result = readMetablockHuffmanCodesAndContextMaps(s);
+          if (result < BROTLI_OK) {
+            return result;
+          }
           s.runningState = MAIN_LOOP;
           continue;
+        }
 
         case MAIN_LOOP:
           if (s.metaBlockLength <= 0) {
             s.runningState = BLOCK_START;
             continue;
           }
-          BitReader.readMoreInput(s);
+          if (s.halfOffset > BitReader.HALF_WATERLINE) {
+            result = BitReader.readMoreInput(s);
+            if (result < BROTLI_OK) {
+              return result;
+            }
+          }
           if (s.commandBlockLength == 0) {
             decodeCommandBlockSwitch(s);
           }
@@ -1152,7 +1325,12 @@
         case INSERT_LOOP:
           if (s.trivialLiteralContext != 0) {
             while (s.j < s.insertLength) {
-              BitReader.readMoreInput(s);
+              if (s.halfOffset > BitReader.HALF_WATERLINE) {
+                result = BitReader.readMoreInput(s);
+                if (result < BROTLI_OK) {
+                  return result;
+                }
+              }
               if (s.literalBlockLength == 0) {
                 decodeLiteralBlockSwitch(s);
               }
@@ -1171,7 +1349,12 @@
             int prevByte1 = (int) ringBuffer[(s.pos - 1) & ringBufferMask] & 0xFF;
             int prevByte2 = (int) ringBuffer[(s.pos - 2) & ringBufferMask] & 0xFF;
             while (s.j < s.insertLength) {
-              BitReader.readMoreInput(s);
+              if (s.halfOffset > BitReader.HALF_WATERLINE) {
+                result = BitReader.readMoreInput(s);
+                if (result < BROTLI_OK) {
+                  return result;
+                }
+              }
               if (s.literalBlockLength == 0) {
                 decodeLiteralBlockSwitch(s);
               }
@@ -1206,7 +1389,12 @@
             // distanceCode in untouched; assigning it 0 won't affect distance ring buffer rolling.
             s.distance = s.rings[s.distRbIdx];
           } else {
-            BitReader.readMoreInput(s);
+            if (s.halfOffset > BitReader.HALF_WATERLINE) {
+              result = BitReader.readMoreInput(s);
+              if (result < BROTLI_OK) {
+                return result;
+              }
+            }
             if (s.distanceBlockLength == 0) {
               decodeDistanceBlockSwitch(s);
             }
@@ -1220,7 +1408,7 @@
                   (s.distRbIdx + DISTANCE_SHORT_CODE_INDEX_OFFSET[distanceCode]) & 0x3;
               s.distance = s.rings[index] + DISTANCE_SHORT_CODE_VALUE_OFFSET[distanceCode];
               if (s.distance < 0) {
-                throw new BrotliRuntimeException("Negative distance"); // COV_NF_LINE
+                return Utils.makeError(s, BROTLI_ERROR_NEGATIVE_DISTANCE);
               }
             } else {
               final int extraBits = (int) s.distExtraBits[distanceCode];
@@ -1253,7 +1441,7 @@
           }
 
           if (s.copyLength > s.metaBlockLength) {
-            throw new BrotliRuntimeException("Invalid backward reference"); // COV_NF_LINE
+            return Utils.makeError(s, BROTLI_ERROR_INVALID_BACKWARD_REFERENCE);
           }
           s.j = 0;
           s.runningState = COPY_LOOP;
@@ -1300,7 +1488,10 @@
           continue;
 
         case USE_DICTIONARY:
-          doUseDictionary(s, fence);
+          result = doUseDictionary(s, fence);
+          if (result < BROTLI_OK) {
+            return result;
+          }
           continue;
 
         case COPY_FROM_COMPOUND_DICTIONARY:
@@ -1308,14 +1499,19 @@
           if (s.pos >= fence) {
             s.nextRunningState = COPY_FROM_COMPOUND_DICTIONARY;
             s.runningState = INIT_WRITE;
-            return;
+            return BROTLI_OK_NEED_MORE_OUTPUT;
           }
           s.runningState = MAIN_LOOP;
           continue;
 
         case READ_METADATA:
           while (s.metaBlockLength > 0) {
-            BitReader.readMoreInput(s);
+            if (s.halfOffset > BitReader.HALF_WATERLINE) {
+              result = BitReader.readMoreInput(s);
+              if (result < BROTLI_OK) {
+                return result;
+              }
+            }
             // Optimize
             BitReader.fillBitWindow(s);
             BitReader.readFewBits(s, 8);
@@ -1325,7 +1521,10 @@
           continue;
 
         case COPY_UNCOMPRESSED:
-          copyUncompressedData(s);
+          result = copyUncompressedData(s);
+          if (result < BROTLI_OK) {
+            return result;
+          }
           continue;
 
         case INIT_WRITE:
@@ -1334,9 +1533,10 @@
           continue;
 
         case WRITE:
-          if (writeRingBuffer(s) == 0) {
+          result = writeRingBuffer(s);
+          if (result != BROTLI_OK) {
             // Output buffer is full.
-            return;
+            return result;
           }
           if (s.pos >= s.maxBackwardDistance) {
             s.maxDistance = s.maxBackwardDistance;
@@ -1353,15 +1553,23 @@
           continue;
 
         default:
-          throw new BrotliRuntimeException("Unexpected state " + String.valueOf(s.runningState));
+          return Utils.makeError(s, BROTLI_PANIC_UNEXPECTED_STATE);
       }
     }
-    if (s.runningState == FINISHED) {
-      if (s.metaBlockLength < 0) {
-        throw new BrotliRuntimeException("Invalid metablock length");
-      }
-      BitReader.jumpToByteBoundary(s);
-      BitReader.checkHealth(s, 1);
+    if (s.runningState != FINISHED) {
+      return Utils.makeError(s, BROTLI_PANIC_UNREACHABLE);
     }
+    if (s.metaBlockLength < 0) {
+      return Utils.makeError(s, BROTLI_ERROR_INVALID_METABLOCK_LENGTH);
+    }
+    result = BitReader.jumpToByteBoundary(s);
+    if (result != BROTLI_OK) {
+      return result;
+    }
+    result = BitReader.checkHealth(s, 1);
+    if (result != BROTLI_OK) {
+      return result;
+    }
+    return BROTLI_OK_DONE;
   }
 }
diff --git a/java/org/brotli/dec/Dictionary.java b/java/org/brotli/dec/Dictionary.java
index e4378e5..9760445 100644
--- a/java/org/brotli/dec/Dictionary.java
+++ b/java/org/brotli/dec/Dictionary.java
@@ -39,19 +39,23 @@
     }
   }
 
+  private static final int DICTIONARY_DEBUG = Utils.isDebugMode();
+
   public static void setData(ByteBuffer newData, int[] newSizeBits) {
-    if ((Utils.isDirect(newData) == 0) || (Utils.isReadOnly(newData) == 0)) {
-      throw new BrotliRuntimeException("newData must be a direct read-only byte buffer");
-    }
-    // TODO: is that so?
-    if (newSizeBits.length > MAX_DICTIONARY_WORD_LENGTH) {
-      throw new BrotliRuntimeException(
-          "sizeBits length must be at most " + String.valueOf(MAX_DICTIONARY_WORD_LENGTH));
-    }
-    for (int i = 0; i < MIN_DICTIONARY_WORD_LENGTH; ++i) {
-      if (newSizeBits[i] != 0) {
+    if (DICTIONARY_DEBUG != 0) {
+      if ((Utils.isDirect(newData) == 0) || (Utils.isReadOnly(newData) == 0)) {
+        throw new BrotliRuntimeException("newData must be a direct read-only byte buffer");
+      }
+      // TODO: is that so?
+      if (newSizeBits.length > MAX_DICTIONARY_WORD_LENGTH) {
         throw new BrotliRuntimeException(
-            "first " + String.valueOf(MIN_DICTIONARY_WORD_LENGTH) + " must be 0");
+            "sizeBits length must be at most " + String.valueOf(MAX_DICTIONARY_WORD_LENGTH));
+      }
+      for (int i = 0; i < MIN_DICTIONARY_WORD_LENGTH; ++i) {
+        if (newSizeBits[i] != 0) {
+          throw new BrotliRuntimeException(
+              "first " + String.valueOf(MIN_DICTIONARY_WORD_LENGTH) + " must be 0");
+        }
       }
     }
     final int[] dictionaryOffsets = Dictionary.offsets;
@@ -65,20 +69,24 @@
       dictionaryOffsets[i] = pos;
       final int bits = dictionarySizeBits[i];
       if (bits != 0) {
-        if (bits >= 31) {
-          throw new BrotliRuntimeException("newSizeBits values must be less than 31");
-        }
-        pos += i << bits;
-        if (pos <= 0 || pos > limit) {
-          throw new BrotliRuntimeException("newSizeBits is inconsistent: overflow");
+        pos += i << (bits & 31);
+        if (DICTIONARY_DEBUG != 0) {
+          if (bits >= 31) {
+            throw new BrotliRuntimeException("newSizeBits values must be less than 31");
+          }
+          if (pos <= 0 || pos > limit) {
+            throw new BrotliRuntimeException("newSizeBits is inconsistent: overflow");
+          }
         }
       }
     }
     for (int i = newSizeBits.length; i < 32; ++i) {
       dictionaryOffsets[i] = pos;
     }
-    if (pos != limit) {
-      throw new BrotliRuntimeException("newSizeBits is inconsistent: underflow");
+    if (DICTIONARY_DEBUG != 0) {
+      if (pos != limit) {
+        throw new BrotliRuntimeException("newSizeBits is inconsistent: underflow");
+      }
     }
     Dictionary.data = newData;
   }
diff --git a/java/org/brotli/dec/DictionaryData.java b/java/org/brotli/dec/DictionaryData.java
index dc3657c..de368a3 100644
--- a/java/org/brotli/dec/DictionaryData.java
+++ b/java/org/brotli/dec/DictionaryData.java
@@ -33,12 +33,16 @@
    */
   private static final String SIZE_BITS_DATA = "AAAAKKLLKKKKKJJIHHIHHGGFF";
 
+  private static final int DICTIONARY_DATA_DEBUG = Utils.isDebugMode();
+
   private static void unpackDictionaryData(ByteBuffer dictionary, String data0, String data1,
       String skipFlip, int[] sizeBits, String sizeBitsData) {
     // Initialize lower 7 bits of every byte in the dictionary.
     final byte[] dict = Utils.toUsAsciiBytes(data0 + data1);
-    if (dict.length != dictionary.capacity()) {
-      throw new RuntimeException("Corrupted brotli dictionary");
+    if (DICTIONARY_DATA_DEBUG != 0) {
+      if (dict.length != dictionary.capacity()) {
+        throw new RuntimeException("Corrupted brotli dictionary");
+      }
     }
 
     // Toggle high bit using run-length delta encoded "skipFlip".
diff --git a/java/org/brotli/dec/SynthTest.java b/java/org/brotli/dec/SynthTest.java
index 9aca0c4..fcfbb9a 100644
--- a/java/org/brotli/dec/SynthTest.java
+++ b/java/org/brotli/dec/SynthTest.java
@@ -50,7 +50,7 @@
       assertArrayEquals(expected, actual);
     } catch (IOException ex) {
       if (expectSuccess) {
-        fail("expected to succeed decoding, but failed");
+        throw new AssertionError("expected to succeed decoding, but failed", ex);
       }
     }
   }
diff --git a/java/org/brotli/dec/Utils.java b/java/org/brotli/dec/Utils.java
index 304cc1b..97fba3d 100644
--- a/java/org/brotli/dec/Utils.java
+++ b/java/org/brotli/dec/Utils.java
@@ -6,6 +6,10 @@
 
 package org.brotli.dec;
 
+import static org.brotli.dec.BrotliError.BROTLI_ERROR_READ_FAILED;
+import static org.brotli.dec.BrotliError.BROTLI_OK;
+import static org.brotli.dec.BrotliError.BROTLI_PANIC;
+
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -29,8 +33,8 @@
    * less than 16.
    *
    * @param dest array to fill with zeroes
-   * @param offset the first byte to fill
-   * @param length number of bytes to change
+   * @param start the first item to fill
+   * @param end the last item to fill (exclusive)
    */
   static void fillBytesWithZeroes(byte[] dest, int start, int end) {
     int cursor = start;
@@ -48,8 +52,8 @@
    * less than 16.
    *
    * @param dest array to fill with zeroes
-   * @param offset the first item to fill
-   * @param length number of item to change
+   * @param start the first item to fill
+   * @param end the last item to fill (exclusive)
    */
   static void fillIntsWithZeroes(int[] dest, int start, int end) {
     int cursor = start;
@@ -72,7 +76,7 @@
     try {
       return s.input.read(dst, offset, length);
     } catch (IOException e) {
-      throw new BrotliRuntimeException("Failed to read input", e);
+      return makeError(s, BROTLI_ERROR_READ_FAILED);
     }
   }
 
@@ -134,4 +138,18 @@
   static int min(int a, int b) {
     return Math.min(a, b);
   }
+
+  static int makeError(State s, int code) {
+    if (code >= BROTLI_OK) {
+      return code;
+    }
+    if (s.runningState >= 0) {
+      s.runningState = code;  // Only the first error is remembered.
+    }
+    // TODO(eustas): expand codes to messages, if ever necessary.
+    if (code <= BROTLI_PANIC) {
+      throw new IllegalStateException("Brotli error code: " + code);
+    }
+    throw new BrotliRuntimeException("Error code: " + code);
+  }
 }