diff --git a/src/main/java/ru/windcorp/jputil/ConstantsMapException.java b/src/main/java/ru/windcorp/jputil/ConstantsMapException.java
new file mode 100644
index 0000000..3c50204
--- /dev/null
+++ b/src/main/java/ru/windcorp/jputil/ConstantsMapException.java
@@ -0,0 +1,45 @@
+/*
+ * JPUtil
+ * Copyright (C)  2019-2021  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+ 
+package ru.windcorp.jputil;
+
+public class ConstantsMapException extends RuntimeException {
+
+	private static final long serialVersionUID = -4298704891780063127L;
+
+	public ConstantsMapException() {
+
+	}
+
+	public ConstantsMapException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+		super(message, cause, enableSuppression, writableStackTrace);
+	}
+
+	public ConstantsMapException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	public ConstantsMapException(String message) {
+		super(message);
+	}
+
+	public ConstantsMapException(Throwable cause) {
+		super(cause);
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/jputil/IntConstantsMap.java b/src/main/java/ru/windcorp/jputil/IntConstantsMap.java
new file mode 100644
index 0000000..08f8101
--- /dev/null
+++ b/src/main/java/ru/windcorp/jputil/IntConstantsMap.java
@@ -0,0 +1,307 @@
+/*
+ * JPUtil
+ * Copyright (C)  2019-2022  OLEGSHA/Javapony and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package ru.windcorp.jputil;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.IntPredicate;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+
+public class IntConstantsMap {
+
+	private final Map namesByValue;
+	private final Map valuesByName;
+
+	protected IntConstantsMap(Map namesByValue, Map valuesByName) {
+		this.namesByValue = namesByValue;
+		this.valuesByName = valuesByName;
+	}
+	
+	public int getValue(String name) {
+		Integer value = valuesByName.get(name);
+		if (value == null) {
+			throw new NoSuchElementException("No constant with name " + name);
+		}
+		return value.intValue();
+	}
+	
+	public boolean hasConstant(String name) {
+		return valuesByName.containsKey(name);
+	}
+	
+	public String getName(int value) {
+		String name = namesByValue.get(value);
+		if (name == null) {
+			throw new NoSuchElementException("No constant with value " + value);
+		}
+		return name;
+	}
+	
+	public boolean hasConstant(int value) {
+		return namesByValue.containsKey(value);
+	}
+	
+	public Map getAll() {
+		return valuesByName;
+	}
+	
+	@Override
+	public String toString() {
+		return valuesByName.toString();
+	}
+
+	public static Builder from(Class> clazz) {
+		return new Builder(clazz);
+	}
+
+	public static class Builder {
+
+		@FunctionalInterface
+		public static interface Filter {
+			boolean test(String name, int value);
+		}
+		
+		public class ConstantSpec {
+			public String name;
+			public int value;
+			
+			public void drop() {
+				if (!extra.contains(name)) {
+					name = null;
+				}
+			}
+		}
+
+		private final List> transforms = new ArrayList<>();
+		private final Set extra = new HashSet<>();
+
+		private final Class> source;
+
+		public Builder(Class> source) {
+			this.source = source;
+		}
+		
+		public Builder apply(Consumer transform) {
+			transforms.add(transform);
+			return this;
+		}
+
+		public Builder only(Filter filter) {
+			return apply(s -> {
+				if (!filter.test(s.name, s.value)) {
+					s.drop();
+				}
+			});
+		}
+
+		public Builder only(Predicate nameFilter) {
+			return apply(s -> {
+				if (!nameFilter.test(s.name)) {
+					s.drop();
+				}
+			});
+		}
+
+		public Builder onlyValued(IntPredicate valueFilter) {
+			return apply(s -> {
+				if (!valueFilter.test(s.value)) {
+					s.drop();
+				}
+			});
+		}
+
+		public Builder regex(String regex) {
+			return only(Pattern.compile(regex).asPredicate());
+		}
+
+		public Builder prefix(String prefix) {
+			return only(n -> n.startsWith(prefix) && n.length() > prefix.length());
+		}
+
+		public Builder exclude(Filter filter) {
+			return only((n, v) -> !filter.test(n, v));
+		}
+
+		public Builder exclude(Predicate nameFilter) {
+			return only(nameFilter.negate());
+		}
+
+		public Builder exclude(String... names) {
+			Set excluded = new HashSet<>();
+			for (String name : names) {
+				excluded.add(name);
+			}
+			return exclude(excluded::contains);
+		}
+
+		public Builder excludeRegex(String... nameRegexes) {
+			List> tests = new ArrayList<>();
+			for (String regex : nameRegexes) {
+				tests.add(Pattern.compile(regex).asPredicate());
+			}
+			return only((n, v) -> {
+				for (Predicate test : tests) {
+					if (test.test(n)) {
+						return false;
+					}
+				}
+
+				return true;
+			});
+		}
+
+		public Builder extra(String... names) {
+			for (String name : names) {
+				extra.add(name);
+			}
+			return this;
+		}
+		
+		public Builder rename(Function renamer) {
+			apply(s -> {
+				s.name = renamer.apply(s.name);
+			});
+			return this;
+		}
+		
+		public Builder stripPrefix(String prefix) {
+			return apply(s -> {
+				if (s.name.startsWith(prefix)) {
+					s.name = s.name.substring(prefix.length());
+				} else if (extra.contains(s.name)) {
+					return;
+				} else {
+					s.drop();
+				}
+			});
+		}
+
+		public IntConstantsMap scan() {
+			return build(true);
+		}
+
+		public IntConstantsMap scanAll() {
+			return build(false);
+		}
+
+		private IntConstantsMap build(boolean onlyPublic) {
+			Map namesByValue = new HashMap<>();
+			Map valuesByName = new HashMap<>();
+
+			BiConsumer putter = (name, value) -> {
+				if (namesByValue.containsKey(value)) {
+					throw newDuplicateException("value", value, name, namesByValue.get(value));
+				}
+				if (valuesByName.containsKey(name)) {
+					throw newDuplicateException("name", name, value, valuesByName.get(name));
+				}
+				namesByValue.put(value, name);
+				valuesByName.put(name, value);
+			};
+
+			try {
+				for (Field field : source.getDeclaredFields()) {
+					processField(field, putter, onlyPublic);
+				}
+			} catch (IllegalAccessException e) {
+				throw new ConstantsMapException(e);
+			}
+
+			return new IntConstantsMap(
+				Collections.unmodifiableMap(namesByValue),
+				Collections.unmodifiableMap(valuesByName)
+			);
+		}
+
+		private void processField(Field field, BiConsumer putter, boolean onlyPublic)
+			throws IllegalAccessException {
+			if (!Modifier.isStatic(field.getModifiers())) {
+				return;
+			}
+			if (!Modifier.isFinal(field.getModifiers())) {
+				return;
+			}
+
+			boolean clearAccessible = false;
+			if (!Modifier.isPublic(field.getModifiers())) {
+				if (onlyPublic) {
+					return;
+				} else if (!isAccessibleFlagSet(field)) {
+					field.setAccessible(true);
+					clearAccessible = true;
+				}
+			}
+
+			try {
+
+				ConstantSpec spec = new ConstantSpec();
+				spec.name = field.getName();
+				spec.value = field.getInt(null);
+				
+				for (Consumer t : transforms) {
+					t.accept(spec);
+					if (spec.name == null) {
+						return;
+					}
+				}
+				
+				putter.accept(spec.name, spec.value);
+
+			} finally {
+				if (clearAccessible) {
+					field.setAccessible(false);
+				}
+			}
+		}
+		
+		/*
+		 * Yes, this method exists only so that neither Java 8 nor Java 9 complain about deprecation.
+		 */
+		@Deprecated
+		private boolean isAccessibleFlagSet(Field f) {
+			return f.isAccessible();
+		}
+
+		private ConstantsMapException newDuplicateException(String what, Object common, Object current, Object old) {
+			return new ConstantsMapException(
+				String.format(
+					"Duplicate %1$s: %2$s -> %3$s and %2$s -> %4$s",
+					what,
+					common,
+					current,
+					old
+				)
+			);
+		}
+
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/backend/GLFWErrorHandler.java b/src/main/java/ru/windcorp/progressia/client/graphics/backend/GLFWErrorHandler.java
new file mode 100644
index 0000000..7f6364d
--- /dev/null
+++ b/src/main/java/ru/windcorp/progressia/client/graphics/backend/GLFWErrorHandler.java
@@ -0,0 +1,56 @@
+/*
+ * Progressia
+ * Copyright (C)  2020-2022  Wind Corporation and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+package ru.windcorp.progressia.client.graphics.backend;
+
+import org.lwjgl.glfw.GLFW;
+import org.lwjgl.glfw.GLFWErrorCallback;
+
+import ru.windcorp.jputil.ConstantsMapException;
+import ru.windcorp.jputil.IntConstantsMap;
+import ru.windcorp.progressia.common.util.crash.CrashReports;
+
+public class GLFWErrorHandler {
+
+	private static final IntConstantsMap ERROR_CODES;
+
+	static {
+		try {
+			ERROR_CODES = IntConstantsMap.from(GLFW.class)
+				.stripPrefix("GLFW_")
+				.onlyValued(i -> i >= 0x10000 && i <= 0x1FFFF)
+				.extra("GLFW_NO_ERROR")
+				.scan();
+		} catch (ConstantsMapException e) {
+			throw CrashReports.report(e, "Could not analyze GLFW error codes");
+		}
+	}
+
+	public void onError(int errorCode, long descriptionPointer) {
+		String description = GLFWErrorCallback.getDescription(descriptionPointer);
+
+		String errorCodeName;
+		if (ERROR_CODES.hasConstant(errorCode)) {
+			errorCodeName = ERROR_CODES.getName(errorCode);
+		} else {
+			errorCodeName = "";
+		}
+
+		throw CrashReports.report(null, "GLFW error detected: " + errorCodeName + " %s", description);
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/backend/LWJGLInitializer.java b/src/main/java/ru/windcorp/progressia/client/graphics/backend/LWJGLInitializer.java
index fded2be..e7d5845 100644
--- a/src/main/java/ru/windcorp/progressia/client/graphics/backend/LWJGLInitializer.java
+++ b/src/main/java/ru/windcorp/progressia/client/graphics/backend/LWJGLInitializer.java
@@ -24,6 +24,7 @@ import static org.lwjgl.system.MemoryUtil.*;
 
 import java.io.IOException;
 
+import org.lwjgl.glfw.GLFWErrorCallback;
 import org.lwjgl.glfw.GLFWImage;
 import org.lwjgl.opengl.GL;
 
@@ -55,6 +56,7 @@ class LWJGLInitializer {
 		setupWindowCallbacks();
 
 		glfwShowWindow(GraphicsBackend.getWindowHandle());
+		GraphicsBackend.onFrameResized(GraphicsBackend.getWindowHandle(), 800, 600);
 	}
 
 	private static void checkEnvironment() {
@@ -62,8 +64,12 @@ class LWJGLInitializer {
 	}
 
 	private static void initializeGLFW() {
-		// TODO Do GLFW error handling: check glfwInit, setup error callback
-		glfwInit();
+		GLFWErrorCallback.create(new GLFWErrorHandler()::onError).set();
+
+		if (!glfwInit()) {
+			throw CrashReports.report(null, "GLFW could not be initialized: glfwInit() has failed");
+		}
+
 		GraphicsBackend.setGLFWInitialized(true);
 	}
 
@@ -71,17 +77,13 @@ class LWJGLInitializer {
 		glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
 		glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
 		glfwWindowHint(GLFW_FOCUSED, GLFW_TRUE);
-		glfwWindowHint(GLFW_MAXIMIZED, GLFW_TRUE);
 
-		long handle = glfwCreateWindow(
-			800,
-			600,
-			Progressia.getName() + " " + Progressia.getFullerVersion(),
-			NULL,
-			NULL
-		);
-
-		// TODO Check that handle != NULL
+		String windowTitle = Progressia.getName() + " " + Progressia.getFullerVersion();
+		long handle = glfwCreateWindow(800, 600, windowTitle, NULL, NULL);
+		
+		if (handle == 0) {
+			throw CrashReports.report(null, "Could not create game window");
+		}
 
 		GraphicsBackend.setWindowHandle(handle);
 
@@ -95,8 +97,8 @@ class LWJGLInitializer {
 	}
 
 	private static void createWindowIcons() {
-		if (glfwGetVersionString().toLowerCase().contains("wayland")) {
-			// glfwSetWindowIcon is not supported on Wayland
+		if (glfwGetPlatform() == GLFW_PLATFORM_WAYLAND) {
+			// Wayland does not support changing window icons
 			return;
 		}