Add detailed error reporting, remove panics
diff --git a/src/lisp/Jmk b/src/lisp/Jmk
index 5b00b6d..ed2e788 100644
--- a/src/lisp/Jmk
+++ b/src/lisp/Jmk
@@ -13,10 +13,16 @@
 archetype(c)
 archetype(asm)
 
+NO_READLINE ?= 0
+
 CFLAGS += -Ivendor/luajit/dynasm -Werror -lreadline # -fsanitize=address
 LDFLAGS += -lreadline
 ASMFLAGS += -felf -Fdwarf
 
+ifeq ($(NO_READLINE),1)
+CFLAGS += -DNO_READLINE
+endif
+
 OBJECTS = main.o \
 			lisp.o \
 			compiler.o \
@@ -24,7 +30,8 @@
 			plat/linux.o \
 			istream.o \
 			gc.o \
-			call_list.o
+			call_list.o \
+			error.o
 
 LUA = vendor/luajit/src/host/minilua
 
diff --git a/src/lisp/compiler.dasc b/src/lisp/compiler.dasc
index 7e71c51..e591ed3 100644
--- a/src/lisp/compiler.dasc
+++ b/src/lisp/compiler.dasc
@@ -131,13 +131,16 @@
 	env->first_loaded = f;
 }
 
-struct dasm_State *compile_function(value_t args, enum namespace namespace,
-                                    struct environment *env,
-                                    struct local *local_out,
-                                    struct local *local_parent,
-                                    struct args **args_out, char *name,
-                                    char *path)
+struct error compile_function(value_t args, enum namespace namespace,
+							  struct environment *env,
+							  struct local *local_out,
+							  struct local *local_parent,
+							  struct args **args_out, char *name,
+							  char *path,
+							  dasm_State **state)
 {
+	E_INIT();
+
 	dasm_State *d;
 	dasm_State **Dst = &d;
 
@@ -172,12 +175,14 @@
 	value_t body = cdr(args);
 
 	// This will add the arguments to local too.
-	struct args *ar = list_to_args(env, arglist, &local);
+	struct args *ar;
+	TRY(list_to_args(env, arglist, &local, &ar));
 	local.args = ar;
 
 	if (!ar)
 	{
-		err("Malformed args list");
+		NEARVAL(arglist);
+		THROW(EMALFORMED, "Malformed argument list");
 	}
 
 	for (value_t body_ = body; !nilp(body_); body_ = cdr(body_))
@@ -193,7 +198,7 @@
 	for (; !nilp(body); body = cdr(body))
 	{
 		bool tail = nilp(cdr(body));
-		compile_expression(env, &local, car(body), tail, Dst);
+		TRY(compile_expression(env, &local, car(body), tail, Dst));
 	}
 
 	| cleanup;
@@ -204,13 +209,21 @@
 	if (args_out)
 		*args_out = ar;
 
-	return d;
+	*state = d;
+
+	OKAY();
 }
 
-void compile_tl(value_t val, struct environment *env, char *fname)
+struct error compile_tl(value_t val, struct environment *env, char *fname)
 {
+	E_INIT();
+
+	NEARVAL(val);
+
 	if (!listp(val))
-		err("Top level must be a list");
+	{
+		THROW(EEXPECTED, "Top level form must be a list");
+	}
 
 	value_t form = car(val);
 	value_t args = cdr(val);
@@ -226,8 +239,9 @@
 		struct args *a;
 		char *name = (char *)(car(args) ^ SYMBOL_TAG);
 
-		dasm_State *d = compile_function(cdr(args), namespace, env, &local,
-		                                 NULL, &a, name, fname);
+		dasm_State *d;
+		TRY(compile_function(cdr(args), namespace, env, &local,
+							 NULL, &a, name, fname, &d));
 
 		add_function(env, name, link_program(&d), a, namespace);
 
@@ -238,18 +252,21 @@
 	{
 		for (value_t val = args; !nilp(val); val = cdr(val))
 		{
-			compile_tl(car(val), env, fname);
+			TRY(compile_tl(car(val), env, fname));
 		}
 	}
 	else if (symstreq(form, "load"))
 	{
 		if (length(args) != 1)
 		{
-			err_at(val, "load expects exactly 1 argument, %d given",
-				   length(args));
+			NEARVAL(args);
+			THROW(EARGS, "load expects exactly 1 argument, %d given",
+				  length(args));
 		}
 		load_relative(env, fname, car(args));
 	}
+
+	OKAY();
 }
 
 void walk_and_alloc(struct local *local, value_t body)
@@ -303,9 +320,10 @@
 
 	value_t val;
 
-	while (read1(is, &val))
+	while (IS_OKAY(read1(is, &val)))
 	{
-		compile_tl(val, env, path);
+		if (!IS_OKAY(compile_tl(val, env, path)))
+			break;
 	}
 
 	del_fistream(is);
@@ -331,22 +349,29 @@
 		return nil;
 }
 
-struct environment *compile_file(char *filename, bool *ok)
+struct error compile_file(char *filename, struct environment **e)
 {
+	E_INIT();
+
 	value_t val;
 	struct environment *env = malloc(sizeof(struct environment));
 	env->first = NULL;
 	env->first_loaded = NULL;
 
 	add_load(env, filename);
-	load_std(env);
+	TRY(load_std(env));
 
 	bool ok_ = load(env, filename);
 
-	if (ok)
-		*ok = ok_;
+	if (!ok_)
+	{
+		free(env);
+		THROWSAFE(ENOTFOUND);
+	}
 
-	return env;
+	*e = env;
+
+	OKAY();
 }
 
 int nextpc(struct local *local, dasm_State **Dst)
@@ -360,9 +385,11 @@
 	return n;
 }
 
-void compile_backquote(struct environment *env, struct local *local,
-                       value_t val, dasm_State **Dst)
+struct error compile_backquote(struct environment *env, struct local *local,
+							   value_t val, dasm_State **Dst)
 {
+	E_INIT();
+
 	if (!listp(val))
 	{
 		| mov eax, (val);
@@ -373,14 +400,16 @@
 		int nargs = length(args),
 			n = length(val);
 
+		NEARVAL(val);
+
 		if (symstreq(fsym, "unquote"))
 		{
 			if (nargs != 1)
 			{
-				err_at(val, "unquote (or ,) takes exactly 1 argument");
+				THROW(EARGS, "unquote (or ,) takes exactly 1 argument");
 			}
 
-			compile_expression(env, local, car(args), false, Dst);
+			TRY(compile_expression(env, local, car(args), false, Dst));
 		}
 		else
 		{
@@ -388,7 +417,7 @@
 
 			for (int i = n - 1; i >= 0; i--)
 			{
-				compile_backquote(env, local, elt(val, i), Dst);
+				TRY(compile_backquote(env, local, elt(val, i), Dst));
 				| push eax;
 				| call_extern cons;
 				| add esp, 8;
@@ -399,6 +428,8 @@
 			| pop eax;
 		}
 	}
+
+	OKAY();
 }
 
 value_t eval(struct environment *env, value_t form)
@@ -409,8 +440,15 @@
 	struct local local;
 	struct args *args;
 
-	dasm_State *d = compile_function(function, NS_ANONYMOUS, env, &local, NULL,
-	                                 &args, NULL, "/");
+	dasm_State *d;
+	struct error err;
+
+	if (!IS_OKAY((err = compile_function(function, NS_ANONYMOUS, env, &local, NULL,
+										 &args, NULL, "/", &d))))
+	{
+		ereport(err);
+		return nil;
+	}
 
 	del_local(&local);
 
@@ -418,8 +456,9 @@
 	return f();
 }
 
-void compile_variable(struct variable *v, dasm_State *Dst)
+struct error compile_variable(struct variable *v, dasm_State *Dst)
 {
+	E_INIT();
 	switch (v->type)
 	{
 	case V_ARGUMENT:
@@ -433,14 +472,18 @@
 		| mov eax, dword[edi + (v->number * value_size)];
 		break;
 	default:
-		err("Sorry, can only access V_ARGUMENT, V_FREE and V_BOUND variables "
-		    "for now :(");
+		THROW(EUNIMPL, "Sorry, can only access V_ARGUMENT, V_BOUND, and V_FREE vars");
 	}
+	OKAY();
 }
 
-void compile_expression(struct environment *env, struct local *local,
-                        value_t val, bool tail, dasm_State **Dst)
+struct error compile_expression(struct environment *env, struct local *local,
+								value_t val, bool tail, dasm_State **Dst)
 {
+	E_INIT();
+
+	NEARVAL(val);
+
 	if (symstreq(val, "nil") || nilp(val))
 	{
 		| mov eax, (nil);
@@ -461,16 +504,17 @@
 
 		if (!symbolp(fsym))
 		{
-			printval(val, 2);
-			err_at(val, "function name must be a symbol");
+			THROW(EEXPECTED, "Function name must be a symbol");
 		}
 
 		if (symstreq(fsym, "if"))
 		{
 			if (nargs < 2 || nargs > 3)
-				err("Must give at least 2 arguments to if");
+			{
+				THROW(EARGS, "Must give at least 2 arguments to if");
+			}
 
-			compile_expression(env, local, car(args), false, Dst);
+			TRY(compile_expression(env, local, car(args), false, Dst));
 			int false_label = nextpc(local, Dst),
 			    after_label = nextpc(local, Dst);
 
@@ -478,11 +522,11 @@
 			| cmp eax, (nil);
 			| je =>false_label;
 
-			compile_expression(env, local, elt(args, 1), tail, Dst);
+			TRY(compile_expression(env, local, elt(args, 1), tail, Dst));
 			| jmp =>after_label;
 			|=>false_label:;
 			if (nargs == 3)
-				compile_expression(env, local, elt(args, 2), tail, Dst);
+				TRY(compile_expression(env, local, elt(args, 2), tail, Dst));
 			|=>after_label:;
 		}
 		else if (symstreq(fsym, "and") || symstreq(fsym, "or"))
@@ -492,14 +536,16 @@
 			// Boolean and and or, short circuit like &&/||
 			if (nargs < 1)
 			{
-				err_at(val, "and & or require at least 1 argument.");
+				THROW(EARGS, "and & or require at least 1 argument.");
 			}
 
 			int after = nextpc(local, Dst);
 
 			for (; !nilp(args); args = cdr(args))
 			{
-				compile_expression(env, local, car(args), false, Dst);
+				NEARVAL(args);
+
+				TRY(compile_expression(env, local, car(args), false, Dst));
 				if (!nilp(cdr(args)))
 				{
 					| cmp eax, nil;
@@ -520,28 +566,33 @@
 		{
 			for (value_t val = args; !nilp(val); val = cdr(val))
 			{
+				NEARVAL(args);
+
 				bool t = tail && nilp(cdr(val));
-				compile_expression(env, local, car(val), t, Dst);
+				TRY(compile_expression(env, local, car(val), t, Dst));
 			}
 		}
 		else if (symstreq(fsym, "let1"))
 		{
 			if (nargs < 2)
 			{
-				err("Must give at least 2 arguments to let1");
+				THROW(EARGS, "Must give at least 2 arguments to let1");
 			}
 			value_t binding = car(args);
 			value_t rest = cdr(args);
 
+			NEARVAL(binding);
 			if (length(binding) != 2)
 			{
-				err("Binding list in let1 must contain exactly two entries");
+				THROW(EARGS, "Binding list in let1 must contain exactly two entries");
 			}
 
+			NEARVAL(rest);
+
 			value_t name = car(binding);
 			value_t value = car(cdr(binding));
 
-			compile_expression(env, local, value, false, Dst);
+			TRY(compile_expression(env, local, value, false, Dst));
 
 			int i = local_alloc(local);
 
@@ -552,7 +603,8 @@
 			for (; !nilp(rest); rest = cdr(rest))
 			{
 				bool t = tail && nilp(cdr(rest));
-				compile_expression(env, local, car(rest), t, Dst);
+				NEARVAL(rest);
+				TRY(compile_expression(env, local, car(rest), t, Dst));
 			}
 
 			local_free(local, i);
@@ -561,7 +613,7 @@
 		{
 			if (nargs)
 			{
-				err_at(val, "gc takes no arguments");
+				THROW(EARGS, "gc takes no arguments");
 			}
 
 			| run_gc;
@@ -569,7 +621,7 @@
 		else if (symstreq(fsym, "quote"))
 		{
 			if (nargs != 1)
-				err("quote should take exactly 1 argument");
+				THROW(EARGS, "quote should take exactly 1 argument");
 
 			// Simple!
 			| mov eax, (car(args));
@@ -577,20 +629,21 @@
 		else if (symstreq(fsym, "backquote"))
 		{
 			if (nargs != 1)
-				err("backquote should take exactly 1 argument");
+				THROW(EARGS, "backquote should take exactly 1 argument");
 
-			compile_backquote(env, local, car(args), Dst);
+			TRY(compile_backquote(env, local, car(args), Dst));
 		}
 		else if (symstreq(fsym, "function"))
 		{
 			if (nargs != 1)
 			{
-				err("function should take exactly 1 argument");
+				THROW(EARGS, "function should take exactly 1 argument");
 			}
 
+			NEARVAL(args);
 			if (!symbolp(car(args)))
 			{
-				err("argument to function should be a symbol resolvable at "
+				THROW(EINVALID, "argument to function should be a symbol resolvable at "
 				    "compile time");
 			}
 
@@ -609,7 +662,7 @@
 
 				if (!f)
 				{
-					err_at(val, "Function `%s' does not exist", (char *)(car(args) ^ SYMBOL_TAG));
+					THROW(EINVALID, "Function `%s' does not exist", (char *)(car(args) ^ SYMBOL_TAG));
 				}
 				value_t closure = create_closure(f->code_ptr, f->args, 0);
 				| mov eax, (closure);
@@ -621,23 +674,24 @@
 
 			for (int i = nargs - 1; i >= 0; i--)
 			{
-				compile_expression(env, local, elt(args, i), false, Dst);
+				TRY(compile_expression(env, local, elt(args, i), false, Dst));
 
 				| push eax;
 				| call_extern cons;
 				| add esp, (2 * value_size);
 				| push eax;
 			}
-				| pop eax;
+			| pop eax;
 		}
 		else if (symstreq(fsym, "lambda"))
 		{
 			// Compile the function with this as the parent scope
 			struct local new_local;
 			int nargs_out;
-			dasm_State *d = compile_function(
-			    args, NS_ANONYMOUS, env, &new_local, local, &nargs_out,
-			    "recurse", local->current_file_path);
+			dasm_State *d;
+			TRY(compile_function(
+					args, NS_ANONYMOUS, env, &new_local, local, &nargs_out,
+					"recurse", local->current_file_path, &d));
 
 			// Link the function
 			void *func_ptr = link_program(&d);
@@ -661,7 +715,7 @@
 					// Closure in eax
 					| push eax;
 					// Variable now in eax
-					compile_variable(find_variable(local, var->name), Dst);
+					TRY(compile_variable(find_variable(local, var->name), Dst));
 					| push eax;
 
 					// The capture offset
@@ -683,10 +737,10 @@
 		{
 			if (nargs != 1)
 			{
-				err("eval takes exactly 1 argument");
+				THROW(EARGS, "eval takes exactly 1 argument");
 			}
 
-			compile_expression(env, local, car(args), false, Dst);
+			TRY(compile_expression(env, local, car(args), false, Dst));
 			| push eax;
 			| push (env);
 			| call_extern eval;
@@ -695,10 +749,10 @@
 		{
 			if (nargs != 1)
 			{
-				err_at(val, "load takes exactly 1 argument, %d given", nargs);
+				THROW(EARGS, "load takes exactly 1 argument, %d given", nargs);
 			}
 
-			compile_expression(env, local, car(args), false, Dst);
+			TRY(compile_expression(env, local, car(args), false, Dst));
 			| push eax;
 			| push (local->current_file_path);
 			| push (env);
@@ -726,7 +780,7 @@
 			{
 				if (func == NULL)
 				{
-					err_at(val, "Function %s undefined", name);
+					THROW(EINVALID, "Function %s undefined", name);
 				}
 
 				nargs_needed = func->args;
@@ -734,11 +788,10 @@
 
 			if (!are_args_acceptable(nargs_needed, nargs))
 			{
-				err_at(val,
-				       "wrong number of args in function call: %s at %s:%d, "
-				       "want %d args but given %d\n",
-				       name, cons_file(val), cons_line(val),
-				       nargs_needed->num_required, nargs);
+				THROW(EARGS,
+					  "wrong number of args in function call: %s, "
+					  "want %d args but given %d\n",
+					  name, nargs_needed->num_required, nargs);
 			}
 
 			int total_taken = nargs_needed->num_optional +
@@ -764,7 +817,7 @@
 
 					for (int i = nargs - 1; i >= total_taken; i--)
 					{
-						compile_expression(env, local, elt(args, i), false, Dst);
+						TRY(compile_expression(env, local, elt(args, i), false, Dst));
 						| push eax;
 						| call_extern cons;
 						| add esp, 8;
@@ -783,7 +836,7 @@
 
 				for (int i = min - 1; i >= 0; i--)
 				{
-					compile_expression(env, local, elt(args, i), false, Dst);
+					TRY(compile_expression(env, local, elt(args, i), false, Dst));
 					| push eax;
 				}
 
@@ -828,7 +881,7 @@
 
 				pop_pool(pool);
 
-				compile_expression(env, local, expanded_to, false, Dst);
+				TRY(compile_expression(env, local, expanded_to, false, Dst));
 			}
 		}
 	}
@@ -847,11 +900,10 @@
 
 			if (!v)
 			{
-				fprintf(stderr, "var: %s\n", (char *)(val ^ SYMBOL_TAG));
-				err("Variable unbound");
+				THROW(EINVALID, "Variable `%s' unbound", (char *)(val ^ SYMBOL_TAG));
 			}
 
-			compile_variable(v, Dst);
+			TRY(compile_variable(v, Dst));
 		}
 	}
 	else if (closurep(val))
@@ -861,8 +913,10 @@
 	else
 	{
 		printval(val, 1);
-		err_at(val, "Don't know how to compile this, sorry.");
+		THROW(EUNIMPL, "Don't know how to compile this, sorry.");
 	}
+
+	OKAY();
 }
 
 struct variable *add_variable(struct local *local, enum var_type type,
@@ -1012,9 +1066,11 @@
 	}
 }
 
-struct args *list_to_args(struct environment *env, value_t list,
-                          struct local *local)
+struct error list_to_args(struct environment *env, value_t list,
+                          struct local *local, struct args **a)
 {
+	E_INIT();
+
 	struct args *args = new_args();
 
 	bool in_optional = false;
@@ -1022,6 +1078,8 @@
 	for (value_t i = list; !nilp(i); i = cdr(i))
 	{
 		value_t val = car(i);
+		NEARVAL(i);
+
 		if (symbolp(val))
 		{
 			if (!args->variadic && symstreq(val, "&"))
@@ -1031,9 +1089,9 @@
 
 				if (!symbolp(name))
 				{
-					err("You must provide a symbol after & in an argument list "
-					    "to bind the\n"
-					    "variadic arguments to.");
+					THROW(EEXPECTED, "You must provide a symbol after & in an argument list "
+						  "to bind the\n"
+						  "variadic arguments to.");
 				}
 
 				args->variadic = true;
@@ -1054,29 +1112,31 @@
 				char *name = (char *)(val ^ SYMBOL_TAG);
 				if (name[0] == '&')
 				{
-					err("Non-optional argument following optional arguments "
-					    "starts with a &\n"
-					    "did you mean to declare a variadic argument? If so "
-					    "leave a space\n"
-					    "between the & and name.");
+					THROW(EINVALID, "Non-optional argument following optional arguments "
+						  "starts with a &\n"
+						  "did you mean to declare a variadic argument? If so "
+						  "leave a space\n"
+						  "between the & and name.");
 				}
 				else
 				{
-					err("Cannot define a non-optional argument after an "
-					    "optional one.");
+					THROW(EINVALID, "Cannot define a non-optional argument after an "
+						  "optional one.");
 				}
 			}
 		}
 		else if (listp(val))
 		{
+			NEARVAL(val);
+
 			in_optional = true;
 			int len = length(val);
 
 			if (len != 2)
 			{
-				err("A list defining an optional value must be structured like "
-				    "(name expr)\n"
-				    "with exactly two arguments.");
+				THROW(EINVALID, "A list defining an optional value must be structured like "
+					  "(name expr)\n"
+					  "with exactly two arguments.");
 			}
 
 			value_t name = car(val);
@@ -1084,9 +1144,9 @@
 
 			value_t function = cons(nil, cons(expr, nil));
 
-			dasm_State *d =
-			    compile_function(function, NS_ANONYMOUS, env, NULL, NULL, NULL,
-			                     NULL, local->current_file_path);
+			dasm_State *d;
+			TRY(compile_function(function, NS_ANONYMOUS, env, NULL, NULL, NULL,
+			                     NULL, local->current_file_path, &d));
 
 			// TODO: GC stack top!
 			value_t (*compiled)() = link_program(&d);
@@ -1099,7 +1159,8 @@
 		}
 	}
 
-	return args;
+	*a = args;
+	OKAY();
 }
 
 void display_args(struct args *args)
diff --git a/src/lisp/compiler.h b/src/lisp/compiler.h
index 5ba6620..67c03e6 100644
--- a/src/lisp/compiler.h
+++ b/src/lisp/compiler.h
@@ -104,16 +104,16 @@
  * `defun`, `defmacro`, `lambda`, etc.
  * @returns NULL if the list is malformed.
  */
-struct args *list_to_args(struct environment *env, value_t list,
-                          struct local *local);
+struct error list_to_args(struct environment *env, value_t list,
+                          struct local *local, struct args **args);
 
 /**
  * Print out `args` to stdout. Useful for debugging.
  */
 void display_args(struct args *args);
 
-void compile_expression(struct environment *env, struct local *local,
-                        value_t val, bool tail, dasm_State **Dst);
+struct error compile_expression(struct environment *env, struct local *local,
+								value_t val, bool tail, dasm_State **Dst) WARN_UNUSED;
 
 /**
  * Compile a function
@@ -130,19 +130,20 @@
  * @returns The compiled function state. You should probably give this to
  * `add_function` or something similar.
  */
-struct dasm_State *compile_function(value_t args, enum namespace namespace,
-                                    struct environment *env,
-                                    struct local *local_out,
-                                    struct local *local_parent,
-                                    struct args **ar, char *name, char *path);
+struct error compile_function(value_t args, enum namespace namespace,
+							  struct environment *env,
+							  struct local *local_out,
+							  struct local *local_parent,
+							  struct args **ar, char *name, char *path,
+							  dasm_State **s) WARN_UNUSED;
 
-void compile_variable(struct variable *v, dasm_State *Dst);
+struct error compile_variable(struct variable *v, dasm_State *Dst) WARN_UNUSED;
 
 /**
  * Compile a backquoted expression
  */
-void compile_backquote(struct environment *env, struct local *local,
-                       value_t val, dasm_State **Dst);
+struct error compile_backquote(struct environment *env, struct local *local,
+							   value_t val, dasm_State **Dst) WARN_UNUSED;
 
 int nextpc(struct local *local, dasm_State **Dst);
 
@@ -172,7 +173,7 @@
  * @param fname The path to the current file.
  * @param val The expression to compile.
  */
-void compile_tl(value_t val, struct environment *env, char *fname);
+struct error compile_tl(value_t val, struct environment *env, char *fname) WARN_UNUSED;
 
 /**
  * Compile a file in a new environment.
@@ -182,7 +183,7 @@
  * @returns The environment for the compiled file, or an empty environment if
  * `ok` was set to `false` (i.e. the file could not be compiled).
  */
-struct environment *compile_file(char *filename, bool *ok);
+struct error compile_file(char *filename, struct environment **env) WARN_UNUSED;
 
 struct function *find_function(struct environment *env, char *name);
 struct variable *add_variable(struct local *local, enum var_type type,
diff --git a/src/lisp/error.c b/src/lisp/error.c
new file mode 100644
index 0000000..266f797
--- /dev/null
+++ b/src/lisp/error.c
@@ -0,0 +1,44 @@
+#include "error.h"
+#include <string.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdio.h>
+
+char *ehsprintf(const char *msg, ...)
+{
+	char *buf = malloc(1024);
+	va_list list;
+	va_start(list, msg);
+	vsnprintf(buf, 1023, msg, list);
+	va_end(list);
+
+	return buf;
+}
+
+void ereport(struct error err)
+{
+	if (err.loc.file && err.loc.line)
+		fprintf(stderr, "\033[31merror at\033[0m %s:%d\n", err.loc.file, err.loc.line);
+	else
+		fprintf(stderr, "\033[31merror\033[0m\n");
+	
+	if (err.message)
+		fprintf(stderr, "%s\n", err.message);
+	else
+	{
+		switch (err.code)
+		{
+		case EEXPECTED:
+			fprintf(stderr, "Expected something but it was not found.\n");
+			break;
+		case EINVALID:
+			fprintf(stderr, "Invalid input.\n");
+			break;
+		case ENOTFOUND:
+			fprintf(stderr, "External resource not found.\n");
+			break;
+		default:
+			fprintf(stderr, "Unknown error %d\n", err.code);
+		}
+	}
+}
diff --git a/src/lisp/error.h b/src/lisp/error.h
new file mode 100644
index 0000000..d4c47e8
--- /dev/null
+++ b/src/lisp/error.h
@@ -0,0 +1,98 @@
+#pragma once
+
+#include <stdbool.h>
+
+// Error handling code
+
+struct eloc
+{
+	int line;
+	char *file;
+};
+
+enum error_code
+{
+	EOK = 0,
+	/// Expected something but didn't get it. if this is in a
+	/// safe_state we should probably just re-try.
+	EEXPECTED,
+	/// An invalid token was present in the input
+	EINVALID,
+	/// A structure was malformed
+	EMALFORMED,
+	/// The arguments provided were invalid
+	EARGS,
+	/// An external resource (say, a file) was not found
+	ENOTFOUND,
+	/// This is unimplemented
+	EUNIMPL,
+};
+
+struct error
+{
+	enum error_code code;
+	// Is any state safe? I.e. can we continue or must we panic?
+	bool safe_state;
+	struct eloc loc;
+	char *message;
+};
+
+#define E_INIT()                                                               \
+	struct error __error;                                                      \
+	__error.code = EOK;                                                        \
+	__error.loc.line = 0;                                                      \
+	__error.safe_state = false;                                                \
+	__error.message = NULL;                                                    \
+	__error.loc.file = NULL;
+#define NEARVAL(val)                                                           \
+	__error.loc.line = cons_line(val);                                         \
+	__error.loc.file = cons_file(val)
+#define NEARIS(is) (is)->getpos((is), &__error.loc.line, &__error.loc.file)
+#define _TRY(expr, m, c)                                                       \
+	{                                                                          \
+		struct error __sub = (expr);                                           \
+		if (__sub.code)                                                        \
+		{                                                                      \
+			if (!__sub.loc.file || !__sub.loc.line)                            \
+				__sub.loc.file = __error.loc.file,                             \
+				__sub.loc.line = __error.loc.line;                             \
+			if (c)                                                             \
+				__sub.code = c;                                                \
+			if (m)                                                             \
+				__sub.message = m;                                             \
+			return __sub;                                                      \
+		}                                                                      \
+	}
+#define TRY(expr) _TRY(expr, NULL, 0)
+#define TRY_ELSE(expr, c, ...) _TRY(expr, ehsprintf(__VA_ARGS__), c)
+#define OKAY() return __error
+#define THROW(_c, ...)                                                         \
+	{                                                                          \
+		__error.code = (_c);                                                   \
+		__error.message = ehsprintf(__VA_ARGS__);                              \
+		return __error;                                                        \
+	}
+#define THROWSAFE(_c)                                                          \
+	{                                                                          \
+		__error.code = (_c);                                                   \
+		__error.safe_state = true;                                             \
+		return __error;                                                        \
+	}
+
+#define IS_OKAY(e) ((e).code == EOK)
+#define OKAY_IF(val)                                                           \
+	{                                                                          \
+		struct error __sub = (val);                                            \
+		if (IS_OKAY(__sub))                                                    \
+			OKAY();                                                            \
+		if (!__sub.safe_state)                                                 \
+			TRY(__sub)                                                         \
+	}
+
+#define WARN_UNUSED __attribute__((warn_unused_result))
+
+// error heap string print formatted
+// returns a heap-allocated string.
+char *ehsprintf(const char *msg, ...);
+
+void ereport(struct error err);
diff --git a/src/lisp/lib/std.c b/src/lisp/lib/std.c
index 85221ea..5ad02bc 100644
--- a/src/lisp/lib/std.c
+++ b/src/lisp/lib/std.c
@@ -89,6 +89,7 @@
 
 value_t l_read_stdin()
 {
+#ifndef NO_READLINE
 	char *string = read_input_line("lisp> ");
 	if (!string)
 		return nil;
@@ -96,12 +97,25 @@
 	struct istream *is = new_stristream_nt(string);
 
 	value_t val = nil;
-	read1(is, &val);
+	struct error err;
+	
+	if (!IS_OKAY((err = read1(is, &val))))
+	{
+		ereport(err);
+
+		del_stristream(is);
+		free(string);
+		// tail recursion, yay!
+		return l_read_stdin();
+	}
 
 	del_stristream(is);
 	free(string);
 
 	return val;
+#else
+	return nil;
+#endif
 }
 
 value_t l_num_eq(value_t a, value_t b)
@@ -114,8 +128,10 @@
 	return (a >> 3) == (b >> 3) ? t : nil;
 }
 
-void load_std(struct environment *env)
+struct error load_std(struct environment *env)
 {
+	E_INIT();
+
 	add_c_function(env, "+", l_plus, 2);
 	add_c_function(env, "-", l_minus, 2);
 	add_c_function(env, "*", l_times, 2);
@@ -135,8 +151,10 @@
 
 	if (!load_library(env, "std"))
 	{
-		err("Could not load library `std`, make sure your $LISP_LIBRARY_PATH is correct.");
+		THROW(ENOTFOUND, "Could not load library `std`, make sure your $LISP_LIBRARY_PATH is correct.");
 	}
+
+	OKAY();
 }
 
 bool load_library(struct environment *env, char *name)
diff --git a/src/lisp/lib/std.h b/src/lisp/lib/std.h
index dae0ac3..eb129d5 100644
--- a/src/lisp/lib/std.h
+++ b/src/lisp/lib/std.h
@@ -8,5 +8,5 @@
 
 void add_function(struct environment *env, char *name, void *func, struct args *args, enum namespace ns);
 void add_c_function(struct environment *env, char *name, void *func, int nargs);
-void load_std(struct environment *env);
+struct error load_std(struct environment *env) WARN_UNUSED;
 bool load_library(struct environment *env, char *name);
diff --git a/src/lisp/lisp.c b/src/lisp/lisp.c
index 6ed3fb9..64ab9ae 100644
--- a/src/lisp/lisp.c
+++ b/src/lisp/lisp.c
@@ -1,4 +1,5 @@
 #include "lisp.h"
+#include "error.h"
 #include "plat/plat.h"
 
 #include <ctype.h>
@@ -15,28 +16,6 @@
 
 unsigned char max_pool = 0, current_pool = 0;
 
-__attribute__((noreturn)) void err(const char *msg)
-{
-	fprintf(stderr, "ERROR: %s\n", msg);
-	exit(1);
-}
-
-__attribute__((noreturn)) void err_at(value_t form, const char *msg, ...)
-{
-	int line = cons_line(form);
-	char *file = cons_file(form);
-
-	fprintf(stderr, "\033[31merror at\033[0m %s:%d\n", file, line);
-
-	va_list list;
-	va_start(list, msg);
-	vfprintf(stderr, msg, list);
-	va_end(list);
-	fprintf(stderr, "\n");
-
-	exit(1);
-}
-
 value_t intval(int i)
 {
 	i <<= 2;
@@ -116,12 +95,14 @@
 	return isalpha(c) || isallowedchar(c) || isdigit(c);
 }
 
-bool readsym(struct istream *is, value_t *val)
+struct error readsym(struct istream *is, value_t *val)
 {
+	E_INIT();
+
 	skipws(is);
 
 	if (!issymstart(is->peek(is)))
-		return false;
+		THROWSAFE(EEXPECTED);
 
 	int size = 8;
 	struct alloc *a = malloc_aligned(size + sizeof(struct alloc));
@@ -150,17 +131,20 @@
 			*val |= SYMBOL_TAG;
 
 			add_this_alloc(a, SYMBOL_TAG);
-			return true;
+
+			OKAY();
 		}
 	}
 }
 
-bool readstr(struct istream *is, value_t *val)
+struct error readstr(struct istream *is, value_t *val)
 {
+	E_INIT();
+
 	skipws(is);
 
 	if (is->peek(is) != '"')
-		return false;
+		THROWSAFE(EEXPECTED);
 
 	bool escape = false;
 	int size = 8;
@@ -209,7 +193,8 @@
 			*val |= STRING_TAG;
 
 			add_this_alloc(a, STRING_TAG);
-			return true;
+
+			OKAY();
 		}
 	}
 }
@@ -265,12 +250,15 @@
 	}
 }
 
-bool readlist(struct istream *is, value_t *val)
+struct error readlist(struct istream *is, value_t *val)
 {
+	E_INIT();
+	NEARIS(is);
+
 	skipws(is);
 
 	if (is->peek(is) != '(')
-		return false;
+		THROWSAFE(EEXPECTED);
 
 	is->get(is);
 
@@ -280,23 +268,24 @@
 
 	if (is->peek(is) != ')')
 	{
-		is->showpos(is, stderr);
-		err("Unterminated list");
-		return false;
+		NEARIS(is);
+		THROW(EEXPECTED, "Unterminated list");
 	}
 	is->get(is);
 
-	return true;
+	OKAY();
 }
 
-bool readint(struct istream *is, value_t *val)
+struct error readint(struct istream *is, value_t *val)
 {
+	E_INIT();
+
 	skipws(is);
 
 	int number = 0;
 
 	if (!isdigit(is->peek(is)))
-		return false;
+		THROWSAFE(EEXPECTED);
 
 	while (isdigit(is->peek(is)))
 	{
@@ -305,11 +294,13 @@
 	}
 
 	*val = intval(number);
-	return true;
+	OKAY();
 }
 
-bool readquote(struct istream *is, value_t *val)
+struct error readquote(struct istream *is, value_t *val)
 {
+	E_INIT();
+
 	skipws(is);
 
 	char c = is->peek(is);
@@ -332,15 +323,9 @@
 		// Read the next form and wrap it in the appropriate function
 
 		value_t wrapped;
-		bool has_next = read1(is, &wrapped);
+		NEARIS(is);
 
-		if (!has_next)
-		{
-			fprintf(stderr, "Expected a form after reader macro char %c\n", c);
-			is->showpos(is, stderr);
-			err("Invalid reader macro");
-			return false;
-		}
+		TRY_ELSE(read1(is, &wrapped), EEXPECTED, "Expected a form after reader macro char %c", c);
 
 		value_t symbol = nil;
 
@@ -362,39 +347,33 @@
 			symbol = symval("function");
 			break;
 		default:
-			is->showpos(is, stderr);
-			err("Something went wrong parsing a reader macro");
+			NEARIS(is);
+			THROW(EINVALID, "Invalid reader macro char %c", c);
 		}
 
 		*val = cons(symbol, cons(wrapped, nil));
 
-		return true;
+		OKAY();
 	}
 	else
 	{
-		return false;
+		THROWSAFE(EEXPECTED);
 	}
 }
 
-bool read1(struct istream *is, value_t *val)
+struct error read1(struct istream *is, value_t *val)
 {
-	// This could all be one big short-circuiting || but that is ugly.
-	if (readquote(is, val))
-		return true;
+	E_INIT();
 
-	if (readsym(is, val))
-		return true;
+	NEARIS(is);
 
-	if (readstr(is, val))
-		return true;
+	OKAY_IF(readquote(is, val));
+	OKAY_IF(readsym(is, val));
+	OKAY_IF(readstr(is, val));
+	OKAY_IF(readint(is, val));
+	OKAY_IF(readlist(is, val));
 
-	if (readint(is, val))
-		return true;
-
-	if (readlist(is, val))
-		return true;
-
-	return false;
+	THROWSAFE(EEXPECTED);
 }
 
 void set_cons_info(value_t cons, int line, char *name)
@@ -415,7 +394,7 @@
 
 	value_t read_val;
 
-	while (read1(is, &read_val))
+	while (IS_OKAY(read1(is, &read_val)))
 	{
 		int line;
 		char *file;
@@ -703,6 +682,7 @@
 	}
 	else
 	{
-		err("Don't know how to deep copy this, sorry... please report this bug :)");
+		fprintf(stderr, "Don't know how to deep copy this, sorry... please report this bug :)");
+		return nil;
 	}
 }
diff --git a/src/lisp/lisp.h b/src/lisp/lisp.h
index c572c80..430e526 100644
--- a/src/lisp/lisp.h
+++ b/src/lisp/lisp.h
@@ -1,5 +1,6 @@
 #pragma once
 
+#include "error.h"
 #include "istream.h"
 #include <stdbool.h>
 #include <stdio.h>
@@ -153,22 +154,21 @@
 
 bool startswith(struct istream *s, char *pattern);
 
-bool readsym(struct istream *is, value_t *val);
-bool readstr(struct istream *is, value_t *val);
-bool readlist(struct istream *is, value_t *val);
-bool readint(struct istream *is, value_t *val);
+struct error readsym(struct istream *is, value_t *val) WARN_UNUSED;
+struct error readstr(struct istream *is, value_t *val) WARN_UNUSED;
+struct error readlist(struct istream *is, value_t *val) WARN_UNUSED;
+struct error readint(struct istream *is, value_t *val) WARN_UNUSED;
 
 /**
  * Read a quoted form, including `'` (quote) `\`` (backquote) and `,` (unquote)
- * @returns true if read successfully, and sets `val`.
  */
-bool readquote(struct istream *is, value_t *val);
+struct error readquote(struct istream *is, value_t *val) WARN_UNUSED;
 
 value_t intval(int i);
 value_t strval(char *str);
 value_t symval(char *str);
 value_t cons(value_t car, value_t cdr);
-bool read1(struct istream *is, value_t *val);
+struct error read1(struct istream *is, value_t *val) WARN_UNUSED;
 value_t read(struct istream *is);
 value_t readn(struct istream *is);
 
@@ -199,9 +199,6 @@
 
 void printval(value_t v, int depth);
 
-void err(const char *msg);
-void err_at(value_t form, const char *msg, ...);
-
 bool symstreq(value_t sym, char *str);
 
 value_t create_closure(void *code, struct args *args, int ncaptures);
diff --git a/src/lisp/main.c b/src/lisp/main.c
index b0c014e..07e6d4a 100644
--- a/src/lisp/main.c
+++ b/src/lisp/main.c
@@ -12,13 +12,13 @@
 	}
 
 	bool ok;
-	struct environment *env = compile_file(argv[1], &ok);
-
-	if (!ok)
+	struct environment *env = NULL;
+	struct error compile_error;
+	if (!IS_OKAY((compile_error = compile_file(argv[1], &env))))
 	{
-		fprintf(stderr, "Could not open %s\n", argv[1]);
-		return 1;
-	}
+		ereport(compile_error);
+		goto done;
+	}		
 
 	value_t (*lisp_main)() = find_function(env, "main")->def0;
 
@@ -32,6 +32,8 @@
 		fprintf(stderr, "No MAIN function defined! nothing to do\n");
 	}
 
+done:
 	free_all();
-	del_env(env);
+	if (env)
+		del_env(env);
 }