diff --git a/src/build-remote/build-remote.cc b/src/build-remote/build-remote.cc
index d2ea6c956b801d83ff72b8ba7cb447f7006c5d0b..49b35a4e9154200fc15c41380da62c59a67588aa 100644
--- a/src/build-remote/build-remote.cc
+++ b/src/build-remote/build-remote.cc
@@ -200,9 +200,12 @@ static int _main(int argc, char * * argv)
 
                 } catch (std::exception & e) {
                     auto msg = chomp(drainFD(5, false));
-                    printError("cannot build on '%s': %s%s",
-                        bestMachine->storeUri, e.what(),
-                        (msg.empty() ? "" : ": " + msg));
+                    logError({ 
+                        .name = "Remote build",
+                        .hint = hintfmt("cannot build on '%s': %s%s",
+                            bestMachine->storeUri, e.what(),
+                            (msg.empty() ? "" : ": " + msg))
+                    });
                     bestMachine->enabled = false;
                     continue;
                 }
diff --git a/src/error-demo/error-demo.cc b/src/error-demo/error-demo.cc
deleted file mode 100644
index a9ff6057c65745b50a00e1318c8315dfaf4c7ea0..0000000000000000000000000000000000000000
--- a/src/error-demo/error-demo.cc
+++ /dev/null
@@ -1,66 +0,0 @@
-#include "error.hh"
-#include "nixexpr.hh"
-
-#include <iostream>
-#include <optional>
-
-int main()
-{
-    using namespace nix;
-
-    // In each program where errors occur, this has to be set.
-    ErrorInfo::programName = std::optional("error-demo");
-
-    // Error in a program; no hint and no nix code.
-    printErrorInfo(
-        ErrorInfo { .level = elError,
-                    .name = "name",
-                    .description = "error description",
-                  });
-
-    // Warning with name, description, and hint.
-    // The hintfmt function makes all the substituted text yellow.
-    printErrorInfo(
-        ErrorInfo { .level = elWarning,
-                    .name = "name",
-                    .description = "error description",
-                    .hint =  std::optional(
-                                 hintfmt("there was a %1%", "warning")),
-                  });
-
-
-    // Warning with nix file, line number, column, and the lines of
-    // code where a warning occurred.
-    SymbolTable testTable;
-    auto problem_file = testTable.create("myfile.nix");
-
-    printErrorInfo(
-        ErrorInfo{
-            .level = elWarning,
-            .name = "warning name",
-            .description = "warning description",
-            .hint = hintfmt("this hint has %1% templated %2%!!", "yellow", "values"),
-            .nixCode = NixCode {
-                .errPos = Pos(problem_file, 40, 13),
-                .prevLineOfCode = std::nullopt,
-                .errLineOfCode = "this is the problem line of code",
-                .nextLineOfCode = std::nullopt
-            }});
-
-    // Error with previous and next lines of code.
-    printErrorInfo(
-        ErrorInfo{
-            .level = elError,
-            .name = "error name",
-            .description = "error description",
-            .hint = hintfmt("this hint has %1% templated %2%!!", "yellow", "values"),
-            .nixCode = NixCode {
-                .errPos = Pos(problem_file, 40, 13),
-                .prevLineOfCode = std::optional("previous line of code"),
-                .errLineOfCode = "this is the problem line of code",
-                .nextLineOfCode = std::optional("next line of code"),
-            }});
-
-
-    return 0;
-}
diff --git a/src/error-demo/local.mk b/src/error-demo/local.mk
deleted file mode 100644
index 2c528490a2ad4c33de1471dd255f5ae18f6ec3b0..0000000000000000000000000000000000000000
--- a/src/error-demo/local.mk
+++ /dev/null
@@ -1,12 +0,0 @@
-programs += error-demo
-
-error-demo_DIR := $(d)
-
-error-demo_SOURCES := \
-  $(wildcard $(d)/*.cc) \
-
-error-demo_CXXFLAGS += -I src/libutil -I src/libexpr
-
-error-demo_LIBS = libutil libexpr
-
-error-demo_LDFLAGS = -pthread $(SODIUM_LIBS) $(EDITLINE_LIBS) $(BOOST_LDFLAGS) -lboost_context -lboost_thread -lboost_system
diff --git a/src/libexpr/attr-path.cc b/src/libexpr/attr-path.cc
index 76d101b98e92443e947e494614debca513d7d9ec..9a9531a3f83890852de70daed59ee562589c976a 100644
--- a/src/libexpr/attr-path.cc
+++ b/src/libexpr/attr-path.cc
@@ -19,7 +19,7 @@ static Strings parseAttrPath(const string & s)
             ++i;
             while (1) {
                 if (i == s.end())
-                    throw Error(format("missing closing quote in selection path '%1%'") % s);
+                    throw Error("missing closing quote in selection path '%1%'", s);
                 if (*i == '"') break;
                 cur.push_back(*i++);
             }
@@ -60,11 +60,11 @@ std::pair<Value *, Pos> findAlongAttrPath(EvalState & state, const string & attr
 
             if (v->type != tAttrs)
                 throw TypeError(
-                    format("the expression selected by the selection path '%1%' should be a set but is %2%")
-                    % attrPath % showType(*v));
-
+                    "the expression selected by the selection path '%1%' should be a set but is %2%",
+                    attrPath,
+                    showType(*v)); 
             if (attr.empty())
-                throw Error(format("empty attribute name in selection path '%1%'") % attrPath);
+                throw Error("empty attribute name in selection path '%1%'", attrPath);
 
             Bindings::iterator a = v->attrs->find(state.symbols.create(attr));
             if (a == v->attrs->end())
@@ -77,9 +77,9 @@ std::pair<Value *, Pos> findAlongAttrPath(EvalState & state, const string & attr
 
             if (!v->isList())
                 throw TypeError(
-                    format("the expression selected by the selection path '%1%' should be a list but is %2%")
-                    % attrPath % showType(*v));
-
+                    "the expression selected by the selection path '%1%' should be a list but is %2%",
+                    attrPath,
+                    showType(*v)); 
             if (attrIndex >= v->listSize())
                 throw AttrPathNotFound("list index %1% in selection path '%2%' is out of range", attrIndex, attrPath);
 
diff --git a/src/libexpr/attr-set.hh b/src/libexpr/attr-set.hh
index 118c7bd5d1fc63d6de5b326df1443149e0812ec4..f5651891f04845417e1fd6b44df934653dd632f8 100644
--- a/src/libexpr/attr-set.hh
+++ b/src/libexpr/attr-set.hh
@@ -76,7 +76,12 @@ public:
     {
         auto a = get(name);
         if (!a)
-            throw Error("attribute '%s' missing, at %s", name, pos);
+            throw Error(
+                ErrorInfo { 
+                    .hint = hintfmt("attribute '%s' missing", name),
+                    .nixCode = NixCode { .errPos = pos }
+                });
+
         return *a;
     }
 
diff --git a/src/libexpr/eval-inline.hh b/src/libexpr/eval-inline.hh
index 942cda1ea128940aacc4951a148ee03c4bfeeebe..eee49e02e3ea3f5c0c8f2e4a3ddcafb23d684e96 100644
--- a/src/libexpr/eval-inline.hh
+++ b/src/libexpr/eval-inline.hh
@@ -7,20 +7,28 @@
 
 namespace nix {
 
-LocalNoInlineNoReturn(void throwEvalError(const char * s, const Pos & pos))
+LocalNoInlineNoReturn(void throwEvalError(const Pos & pos, const char * s))
 {
-    throw EvalError(format(s) % pos);
+    throw EvalError(
+        ErrorInfo { 
+            .hint = hintfmt(s),
+            .nixCode = NixCode { .errPos = pos }
+        });
 }
 
 LocalNoInlineNoReturn(void throwTypeError(const char * s, const Value & v))
 {
-    throw TypeError(format(s) % showType(v));
+    throw TypeError(s, showType(v));
 }
 
 
-LocalNoInlineNoReturn(void throwTypeError(const char * s, const Value & v, const Pos & pos))
+LocalNoInlineNoReturn(void throwTypeError(const Pos & pos, const char * s, const Value & v))
 {
-    throw TypeError(format(s) % showType(v) % pos);
+    throw TypeError(
+        ErrorInfo { 
+            .hint = hintfmt(s, showType(v)),
+            .nixCode = NixCode { .errPos = pos }
+        });
 }
 
 
@@ -43,7 +51,7 @@ void EvalState::forceValue(Value & v, const Pos & pos)
     else if (v.type == tApp)
         callFunction(*v.app.left, *v.app.right, v, noPos);
     else if (v.type == tBlackhole)
-        throwEvalError("infinite recursion encountered, at %1%", pos);
+        throwEvalError(pos, "infinite recursion encountered");
 }
 
 
@@ -59,7 +67,7 @@ inline void EvalState::forceAttrs(Value & v, const Pos & pos)
 {
     forceValue(v, pos);
     if (v.type != tAttrs)
-        throwTypeError("value is %1% while a set was expected, at %2%", v, pos);
+        throwTypeError(pos, "value is %1% while a set was expected", v);
 }
 
 
@@ -75,7 +83,7 @@ inline void EvalState::forceList(Value & v, const Pos & pos)
 {
     forceValue(v, pos);
     if (!v.isList())
-        throwTypeError("value is %1% while a list was expected, at %2%", v, pos);
+        throwTypeError(pos, "value is %1% while a list was expected", v);
 }
 
 /* Note: Various places expect the allocated memory to be zeroed. */
diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc
index 82eb1582e8a69446fb6de076e9ae277ad1c7e2e9..7bf25ea17cfb16a38c19c1b4e44afe512926324d 100644
--- a/src/libexpr/eval.cc
+++ b/src/libexpr/eval.cc
@@ -501,52 +501,81 @@ Value & EvalState::getBuiltin(const string & name)
 
 LocalNoInlineNoReturn(void throwEvalError(const char * s, const string & s2))
 {
-    throw EvalError(format(s) % s2);
+    throw EvalError(s, s2);
 }
 
-LocalNoInlineNoReturn(void throwEvalError(const char * s, const string & s2, const Pos & pos))
+LocalNoInlineNoReturn(void throwEvalError(const Pos & pos, const char * s, const string & s2))
 {
-    throw EvalError(format(s) % s2 % pos);
+    throw EvalError(
+        ErrorInfo { 
+            .hint = hintfmt(s, s2),
+            .nixCode = NixCode { .errPos = pos }
+        });
 }
 
 LocalNoInlineNoReturn(void throwEvalError(const char * s, const string & s2, const string & s3))
 {
-    throw EvalError(format(s) % s2 % s3);
+    throw EvalError(s, s2, s3);
 }
 
-LocalNoInlineNoReturn(void throwEvalError(const char * s, const string & s2, const string & s3, const Pos & pos))
+LocalNoInlineNoReturn(void throwEvalError(const Pos & pos, const char * s, const string & s2, const string & s3))
 {
-    throw EvalError(format(s) % s2 % s3 % pos);
+    throw EvalError(
+        ErrorInfo { 
+            .hint = hintfmt(s, s2, s3),
+            .nixCode = NixCode { .errPos = pos }
+        });
 }
 
-LocalNoInlineNoReturn(void throwEvalError(const char * s, const Symbol & sym, const Pos & p1, const Pos & p2))
+LocalNoInlineNoReturn(void throwEvalError(const Pos & p1, const char * s, const Symbol & sym, const Pos & p2))
 {
-    throw EvalError(format(s) % sym % p1 % p2);
+    // p1 is where the error occurred; p2 is a position mentioned in the message.
+    throw EvalError(
+        ErrorInfo { 
+            .hint = hintfmt(s, sym, p2),
+            .nixCode = NixCode { .errPos = p1 }
+        });
 }
 
-LocalNoInlineNoReturn(void throwTypeError(const char * s, const Pos & pos))
+LocalNoInlineNoReturn(void throwTypeError(const Pos & pos, const char * s))
 {
-    throw TypeError(format(s) % pos);
+    throw TypeError(
+        ErrorInfo { 
+            .hint = hintfmt(s),
+            .nixCode = NixCode { .errPos = pos }
+        });
 }
 
 LocalNoInlineNoReturn(void throwTypeError(const char * s, const string & s1))
 {
-    throw TypeError(format(s) % s1);
+    throw TypeError(s, s1);
 }
 
-LocalNoInlineNoReturn(void throwTypeError(const char * s, const ExprLambda & fun, const Symbol & s2, const Pos & pos))
+LocalNoInlineNoReturn(void throwTypeError(const Pos & pos, const char * s, const ExprLambda & fun, const Symbol & s2))
 {
-    throw TypeError(format(s) % fun.showNamePos() % s2 % pos);
+    throw TypeError(
+        ErrorInfo { 
+            .hint = hintfmt(s, fun.showNamePos(), s2),
+            .nixCode = NixCode { .errPos = pos }
+        });
 }
 
-LocalNoInlineNoReturn(void throwAssertionError(const char * s, const string & s1, const Pos & pos))
+LocalNoInlineNoReturn(void throwAssertionError(const Pos & pos, const char * s, const string & s1))
 {
-    throw AssertionError(format(s) % s1 % pos);
+    throw AssertionError(
+        ErrorInfo { 
+            .hint = hintfmt(s, s1),
+            .nixCode = NixCode { .errPos = pos }
+        });
 }
 
-LocalNoInlineNoReturn(void throwUndefinedVarError(const char * s, const string & s1, const Pos & pos))
+LocalNoInlineNoReturn(void throwUndefinedVarError(const Pos & pos, const char * s, const string & s1))
 {
-    throw UndefinedVarError(format(s) % s1 % pos);
+    throw UndefinedVarError(
+        ErrorInfo { 
+            .hint = hintfmt(s, s1),
+            .nixCode = NixCode { .errPos = pos }
+        });
 }
 
 LocalNoInline(void addErrorPrefix(Error & e, const char * s, const string & s2))
@@ -614,7 +643,7 @@ inline Value * EvalState::lookupVar(Env * env, const ExprVar & var, bool noEval)
             return j->value;
         }
         if (!env->prevWith)
-            throwUndefinedVarError("undefined variable '%1%' at %2%", var.name, var.pos);
+            throwUndefinedVarError(var.pos, "undefined variable '%1%'", var.name);
         for (size_t l = env->prevWith; l; --l, env = env->up) ;
     }
 }
@@ -812,7 +841,7 @@ inline bool EvalState::evalBool(Env & env, Expr * e, const Pos & pos)
     Value v;
     e->eval(*this, env, v);
     if (v.type != tBool)
-        throwTypeError("value is %1% while a Boolean was expected, at %2%", v, pos);
+        throwTypeError(pos, "value is %1% while a Boolean was expected", v);
     return v.boolean;
 }
 
@@ -926,7 +955,7 @@ void ExprAttrs::eval(EvalState & state, Env & env, Value & v)
         Symbol nameSym = state.symbols.create(nameVal.string.s);
         Bindings::iterator j = v.attrs->find(nameSym);
         if (j != v.attrs->end())
-            throwEvalError("dynamic attribute '%1%' at %2% already defined at %3%", nameSym, i.pos, *j->pos);
+            throwEvalError(i.pos, "dynamic attribute '%1%' already defined at %2%", nameSym, *j->pos);
 
         i.valueExpr->setName(nameSym);
         /* Keep sorted order so find can catch duplicates */
@@ -1014,7 +1043,7 @@ void ExprSelect::eval(EvalState & state, Env & env, Value & v)
             } else {
                 state.forceAttrs(*vAttrs, pos);
                 if ((j = vAttrs->attrs->find(name)) == vAttrs->attrs->end())
-                    throwEvalError("attribute '%1%' missing, at %2%", name, pos);
+                    throwEvalError(pos, "attribute '%1%' missing", name);
             }
             vAttrs = j->value;
             pos2 = j->pos;
@@ -1140,7 +1169,7 @@ void EvalState::callFunction(Value & fun, Value & arg, Value & v, const Pos & po
     }
 
     if (fun.type != tLambda)
-        throwTypeError("attempt to call something which is not a function but %1%, at %2%", fun, pos);
+        throwTypeError(pos, "attempt to call something which is not a function but %1%", fun);
 
     ExprLambda & lambda(*fun.lambda.fun);
 
@@ -1168,8 +1197,8 @@ void EvalState::callFunction(Value & fun, Value & arg, Value & v, const Pos & po
         for (auto & i : lambda.formals->formals) {
             Bindings::iterator j = arg.attrs->find(i.name);
             if (j == arg.attrs->end()) {
-                if (!i.def) throwTypeError("%1% called without required argument '%2%', at %3%",
-                    lambda, i.name, pos);
+                if (!i.def) throwTypeError(pos, "%1% called without required argument '%2%'",
+                    lambda, i.name);
                 env2.values[displ++] = i.def->maybeThunk(*this, env2);
             } else {
                 attrsUsed++;
@@ -1184,7 +1213,7 @@ void EvalState::callFunction(Value & fun, Value & arg, Value & v, const Pos & po
                user. */
             for (auto & i : *arg.attrs)
                 if (lambda.formals->argNames.find(i.name) == lambda.formals->argNames.end())
-                    throwTypeError("%1% called with unexpected argument '%2%', at %3%", lambda, i.name, pos);
+                    throwTypeError(pos, "%1% called with unexpected argument '%2%'", lambda, i.name);
             abort(); // can't happen
         }
     }
@@ -1273,7 +1302,7 @@ void ExprAssert::eval(EvalState & state, Env & env, Value & v)
     if (!state.evalBool(env, cond, pos)) {
         std::ostringstream out;
         cond->show(out);
-        throwAssertionError("assertion '%1%' failed at %2%", out.str(), pos);
+        throwAssertionError(pos, "assertion '%1%' failed at %2%", out.str());
     }
     body->eval(state, env, v);
 }
@@ -1425,14 +1454,14 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
                 nf = n;
                 nf += vTmp.fpoint;
             } else
-                throwEvalError("cannot add %1% to an integer, at %2%", showType(vTmp), pos);
+                throwEvalError(pos, "cannot add %1% to an integer", showType(vTmp));
         } else if (firstType == tFloat) {
             if (vTmp.type == tInt) {
                 nf += vTmp.integer;
             } else if (vTmp.type == tFloat) {
                 nf += vTmp.fpoint;
             } else
-                throwEvalError("cannot add %1% to a float, at %2%", showType(vTmp), pos);
+                throwEvalError(pos, "cannot add %1% to a float", showType(vTmp));
         } else
             s << state.coerceToString(pos, vTmp, context, false, firstType == tString);
     }
@@ -1443,7 +1472,7 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
         mkFloat(v, nf);
     else if (firstType == tPath) {
         if (!context.empty())
-            throwEvalError("a string that refers to a store path cannot be appended to a path, at %1%", pos);
+            throwEvalError(pos, "a string that refers to a store path cannot be appended to a path");
         auto path = canonPath(s.str());
         mkPath(v, path.c_str());
     } else
@@ -1492,7 +1521,7 @@ NixInt EvalState::forceInt(Value & v, const Pos & pos)
 {
     forceValue(v, pos);
     if (v.type != tInt)
-        throwTypeError("value is %1% while an integer was expected, at %2%", v, pos);
+        throwTypeError(pos, "value is %1% while an integer was expected", v);
     return v.integer;
 }
 
@@ -1503,7 +1532,7 @@ NixFloat EvalState::forceFloat(Value & v, const Pos & pos)
     if (v.type == tInt)
         return v.integer;
     else if (v.type != tFloat)
-        throwTypeError("value is %1% while a float was expected, at %2%", v, pos);
+        throwTypeError(pos, "value is %1% while a float was expected", v);
     return v.fpoint;
 }
 
@@ -1512,7 +1541,7 @@ bool EvalState::forceBool(Value & v, const Pos & pos)
 {
     forceValue(v, pos);
     if (v.type != tBool)
-        throwTypeError("value is %1% while a Boolean was expected, at %2%", v, pos);
+        throwTypeError(pos, "value is %1% while a Boolean was expected", v);
     return v.boolean;
 }
 
@@ -1527,7 +1556,7 @@ void EvalState::forceFunction(Value & v, const Pos & pos)
 {
     forceValue(v, pos);
     if (v.type != tLambda && v.type != tPrimOp && v.type != tPrimOpApp && !isFunctor(v))
-        throwTypeError("value is %1% while a function was expected, at %2%", v, pos);
+        throwTypeError(pos, "value is %1% while a function was expected", v);
 }
 
 
@@ -1536,7 +1565,7 @@ string EvalState::forceString(Value & v, const Pos & pos)
     forceValue(v, pos);
     if (v.type != tString) {
         if (pos)
-            throwTypeError("value is %1% while a string was expected, at %2%", v, pos);
+            throwTypeError(pos, "value is %1% while a string was expected", v);
         else
             throwTypeError("value is %1% while a string was expected", v);
     }
@@ -1565,8 +1594,8 @@ string EvalState::forceStringNoCtx(Value & v, const Pos & pos)
     string s = forceString(v, pos);
     if (v.string.context) {
         if (pos)
-            throwEvalError("the string '%1%' is not allowed to refer to a store path (such as '%2%'), at %3%",
-                v.string.s, v.string.context[0], pos);
+            throwEvalError(pos, "the string '%1%' is not allowed to refer to a store path (such as '%2%')", 
+                v.string.s, v.string.context[0]);
         else
             throwEvalError("the string '%1%' is not allowed to refer to a store path (such as '%2%')",
                 v.string.s, v.string.context[0]);
@@ -1622,7 +1651,7 @@ string EvalState::coerceToString(const Pos & pos, Value & v, PathSet & context,
             return *maybeString;
         }
         auto i = v.attrs->find(sOutPath);
-        if (i == v.attrs->end()) throwTypeError("cannot coerce a set to a string, at %1%", pos);
+        if (i == v.attrs->end()) throwTypeError(pos, "cannot coerce a set to a string");
         return coerceToString(pos, *i->value, context, coerceMore, copyToStore);
     }
 
@@ -1653,7 +1682,7 @@ string EvalState::coerceToString(const Pos & pos, Value & v, PathSet & context,
         }
     }
 
-    throwTypeError("cannot coerce %1% to a string, at %2%", v, pos);
+    throwTypeError(pos, "cannot coerce %1% to a string", v);
 }
 
 
@@ -1684,7 +1713,7 @@ Path EvalState::coerceToPath(const Pos & pos, Value & v, PathSet & context)
 {
     string path = coerceToString(pos, v, context, false, false);
     if (path == "" || path[0] != '/')
-        throwEvalError("string '%1%' doesn't represent an absolute path, at %2%", path, pos);
+        throwEvalError(pos, "string '%1%' doesn't represent an absolute path", path);
     return path;
 }
 
@@ -1891,8 +1920,11 @@ void EvalState::printStats()
 
 string ExternalValueBase::coerceToString(const Pos & pos, PathSet & context, bool copyMore, bool copyToStore) const
 {
-    throw TypeError(format("cannot coerce %1% to a string, at %2%") %
-        showType() % pos);
+    throw TypeError(
+        ErrorInfo { 
+            .hint = hintfmt("cannot coerce %1% to a string", showType()),
+            .nixCode = NixCode { .errPos = pos }
+        });
 }
 
 
diff --git a/src/libexpr/lexer.l b/src/libexpr/lexer.l
index c34e5c38392381967cfe93e96231f40a55e0fbe8..85376a08f1bdf7a456b35abe9e73e9d3beec6645 100644
--- a/src/libexpr/lexer.l
+++ b/src/libexpr/lexer.l
@@ -127,14 +127,14 @@ or          { return OR_KW; }
               try {
                   yylval->n = boost::lexical_cast<int64_t>(yytext);
               } catch (const boost::bad_lexical_cast &) {
-                  throw ParseError(format("invalid integer '%1%'") % yytext);
+                  throw ParseError("invalid integer '%1%'", yytext);
               }
               return INT;
             }
 {FLOAT}     { errno = 0;
               yylval->nf = strtod(yytext, 0);
               if (errno != 0)
-                  throw ParseError(format("invalid float '%1%'") % yytext);
+                  throw ParseError("invalid float '%1%'", yytext);
               return FLOAT;
             }
 
diff --git a/src/libexpr/nixexpr.cc b/src/libexpr/nixexpr.cc
index 63cbef1ddf84c8df2ec3ea030492d2bd11c22215..91a50830542cb65cb97924a4f979ba0ec2d65efd 100644
--- a/src/libexpr/nixexpr.cc
+++ b/src/libexpr/nixexpr.cc
@@ -267,8 +267,12 @@ void ExprVar::bindVars(const StaticEnv & env)
     /* Otherwise, the variable must be obtained from the nearest
        enclosing `with'.  If there is no `with', then we can issue an
        "undefined variable" error now. */
-    if (withLevel == -1) throw UndefinedVarError(format("undefined variable '%1%' at %2%") % name % pos);
-
+    if (withLevel == -1) 
+        throw UndefinedVarError(
+            ErrorInfo {
+                .hint = hintfmt("undefined variable '%1%'", name),
+                .nixCode = NixCode { .errPos = pos }
+            });
     fromWith = true;
     this->level = withLevel;
 }
diff --git a/src/libexpr/nixexpr.hh b/src/libexpr/nixexpr.hh
index 25798cac66e07ec421d842f5ec449ae7788594ab..47d0e85ec7f2f487df9bb8b5e4541307f9a1105e 100644
--- a/src/libexpr/nixexpr.hh
+++ b/src/libexpr/nixexpr.hh
@@ -2,6 +2,7 @@
 
 #include "value.hh"
 #include "symbol-table.hh"
+#include "error.hh"
 
 #include <map>
 
@@ -235,8 +236,11 @@ struct ExprLambda : Expr
         : pos(pos), arg(arg), matchAttrs(matchAttrs), formals(formals), body(body)
     {
         if (!arg.empty() && formals && formals->argNames.find(arg) != formals->argNames.end())
-            throw ParseError(format("duplicate formal function argument '%1%' at %2%")
-                % arg % pos);
+            throw ParseError(
+                ErrorInfo {
+                    .hint = hintfmt("duplicate formal function argument '%1%'", arg),
+                    .nixCode = NixCode { .errPos = pos }
+                });
     };
     void setName(Symbol & name);
     string showNamePos() const;
diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y
index 1993fa6c1030b82130ebd3c671b7cf91b6a3f026..0417a3c217fb8eca7252481482265503cd71a601 100644
--- a/src/libexpr/parser.y
+++ b/src/libexpr/parser.y
@@ -31,7 +31,7 @@ namespace nix {
         Expr * result;
         Path basePath;
         Symbol path;
-        string error;
+        ErrorInfo error;
         Symbol sLetBody;
         ParseData(EvalState & state)
             : state(state)
@@ -64,15 +64,23 @@ namespace nix {
 
 static void dupAttr(const AttrPath & attrPath, const Pos & pos, const Pos & prevPos)
 {
-    throw ParseError(format("attribute '%1%' at %2% already defined at %3%")
-        % showAttrPath(attrPath) % pos % prevPos);
+    throw ParseError(
+        ErrorInfo {
+            .hint = hintfmt("attribute '%1%' already defined at %2%",
+                showAttrPath(attrPath), prevPos),
+            .nixCode = NixCode { .errPos = pos },
+        });
 }
 
 
 static void dupAttr(Symbol attr, const Pos & pos, const Pos & prevPos)
 {
-    throw ParseError(format("attribute '%1%' at %2% already defined at %3%")
-        % attr % pos % prevPos);
+    throw ParseError(
+        ErrorInfo {
+            .hint = hintfmt("attribute '%1%' already defined at %2%",
+                attr, prevPos),
+            .nixCode = NixCode { .errPos = pos },
+        });
 }
 
 
@@ -140,8 +148,12 @@ static void addAttr(ExprAttrs * attrs, AttrPath & attrPath,
 static void addFormal(const Pos & pos, Formals * formals, const Formal & formal)
 {
     if (!formals->argNames.insert(formal.name).second)
-        throw ParseError(format("duplicate formal function argument '%1%' at %2%")
-            % formal.name % pos);
+        throw ParseError(
+            ErrorInfo {
+                .hint = hintfmt("duplicate formal function argument '%1%'",
+                    formal.name),
+                .nixCode = NixCode { .errPos = pos },
+            });
     formals->formals.push_front(formal);
 }
 
@@ -249,8 +261,10 @@ static inline Pos makeCurPos(const YYLTYPE & loc, ParseData * data)
 
 void yyerror(YYLTYPE * loc, yyscan_t scanner, ParseData * data, const char * error)
 {
-    data->error = (format("%1%, at %2%")
-        % error % makeCurPos(*loc, data)).str();
+    data->error = ErrorInfo { 
+        .hint = hintfmt(error),
+        .nixCode = NixCode { .errPos = makeCurPos(*loc, data) }
+        };
 }
 
 
@@ -327,8 +341,11 @@ expr_function
     { $$ = new ExprWith(CUR_POS, $2, $4); }
   | LET binds IN expr_function
     { if (!$2->dynamicAttrs.empty())
-        throw ParseError(format("dynamic attributes not allowed in let at %1%")
-            % CUR_POS);
+        throw ParseError(
+            ErrorInfo {
+                .hint = hintfmt("dynamic attributes not allowed in let"),
+                .nixCode = NixCode { .errPos = CUR_POS },
+            });
       $$ = new ExprLet($2, $4);
     }
   | expr_if
@@ -405,7 +422,11 @@ expr_simple
   | URI {
       static bool noURLLiterals = settings.isExperimentalFeatureEnabled("no-url-literals");
       if (noURLLiterals)
-          throw ParseError("URL literals are disabled, at %s", CUR_POS);
+          throw ParseError(
+              ErrorInfo { 
+                  .hint = hintfmt("URL literals are disabled"),
+                  .nixCode = NixCode { .errPos = CUR_POS }
+              });
       $$ = new ExprString(data->symbols.create($1));
   }
   | '(' expr ')' { $$ = $2; }
@@ -475,8 +496,11 @@ attrs
           $$->push_back(AttrName(str->s));
           delete str;
       } else
-          throw ParseError(format("dynamic attributes not allowed in inherit at %1%")
-              % makeCurPos(@2, data));
+          throw ParseError(
+              ErrorInfo {
+                  .hint = hintfmt("dynamic attributes not allowed in inherit"),
+                  .nixCode = NixCode { .errPos = makeCurPos(@2, data) },
+              });
     }
   | { $$ = new AttrPath; }
   ;
@@ -671,11 +695,11 @@ Path EvalState::findFile(SearchPath & searchPath, const string & path, const Pos
         Path res = r.second + suffix;
         if (pathExists(res)) return canonPath(res);
     }
-    format f = format(
-        "file '%1%' was not found in the Nix search path (add it using $NIX_PATH or -I)"
-        + string(pos ? ", at %2%" : ""));
-    f.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit);
-    throw ThrownError(f % path % pos);
+    throw ThrownError(
+        ErrorInfo { 
+            .hint = hintfmt("file '%1%' was not found in the Nix search path (add it using $NIX_PATH or -I)", path),
+            .nixCode = NixCode { .errPos = pos }
+        });
 }
 
 
@@ -691,7 +715,11 @@ std::pair<bool, std::string> EvalState::resolveSearchPathElem(const SearchPathEl
             res = { true, store->toRealPath(fetchers::downloadTarball(
                         store, resolveUri(elem.second), "source", false).storePath) };
         } catch (FileTransferError & e) {
-            printError(format("warning: Nix search path entry '%1%' cannot be downloaded, ignoring") % elem.second);
+            logWarning(
+                ErrorInfo { 
+                    .name = "Entry download",
+                    .hint = hintfmt("Nix search path entry '%1%' cannot be downloaded, ignoring", elem.second)
+            });
             res = { false, "" };
         }
     } else {
@@ -699,7 +727,11 @@ std::pair<bool, std::string> EvalState::resolveSearchPathElem(const SearchPathEl
         if (pathExists(path))
             res = { true, path };
         else {
-            printError(format("warning: Nix search path entry '%1%' does not exist, ignoring") % elem.second);
+            logWarning(
+                ErrorInfo { 
+                    .name = "Entry not found",
+                    .hint = hintfmt("warning: Nix search path entry '%1%' does not exist, ignoring", elem.second)
+            });
             res = { false, "" };
         }
     }
diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc
index eb3116d81ac763ef2fd1f2cb2eef91a3f9c066de..23ab7dec656ea3c0eefdcf93b0f6680716920c3a 100644
--- a/src/libexpr/primops.cc
+++ b/src/libexpr/primops.cc
@@ -93,8 +93,12 @@ static void prim_scopedImport(EvalState & state, const Pos & pos, Value * * args
     try {
         state.realiseContext(context);
     } catch (InvalidPathError & e) {
-        throw EvalError(format("cannot import '%1%', since path '%2%' is not valid, at %3%")
-            % path % e.path % pos);
+        throw EvalError(
+            ErrorInfo { 
+                .hint = hintfmt("cannot import '%1%', since path '%2%' is not valid",
+                        path, e.path),
+                .nixCode = NixCode { .errPos = pos }
+            });
     }
 
     Path realPath = state.checkSourcePath(state.toRealPath(path, context));
@@ -170,8 +174,13 @@ void prim_importNative(EvalState & state, const Pos & pos, Value * * args, Value
     try {
         state.realiseContext(context);
     } catch (InvalidPathError & e) {
-        throw EvalError(format("cannot import '%1%', since path '%2%' is not valid, at %3%")
-            % path % e.path % pos);
+        throw EvalError(
+            ErrorInfo {
+                .hint = hintfmt(
+                    "cannot import '%1%', since path '%2%' is not valid",
+                    path, e.path),
+                .nixCode = NixCode { .errPos = pos }
+            });
     }
 
     path = state.checkSourcePath(path);
@@ -180,17 +189,17 @@ void prim_importNative(EvalState & state, const Pos & pos, Value * * args, Value
 
     void *handle = dlopen(path.c_str(), RTLD_LAZY | RTLD_LOCAL);
     if (!handle)
-        throw EvalError(format("could not open '%1%': %2%") % path % dlerror());
+        throw EvalError("could not open '%1%': %2%", path, dlerror());
 
     dlerror();
     ValueInitializer func = (ValueInitializer) dlsym(handle, sym.c_str());
     if(!func) {
         char *message = dlerror();
         if (message)
-            throw EvalError(format("could not load symbol '%1%' from '%2%': %3%") % sym % path % message);
+            throw EvalError("could not load symbol '%1%' from '%2%': %3%", sym, path, message);
         else
-            throw EvalError(format("symbol '%1%' from '%2%' resolved to NULL when a function pointer was expected")
-                    % sym % path);
+            throw EvalError("symbol '%1%' from '%2%' resolved to NULL when a function pointer was expected",
+                sym, path);
     }
 
     (func)(state, v);
@@ -206,7 +215,11 @@ void prim_exec(EvalState & state, const Pos & pos, Value * * args, Value & v)
     auto elems = args[0]->listElems();
     auto count = args[0]->listSize();
     if (count == 0) {
-        throw EvalError(format("at least one argument to 'exec' required, at %1%") % pos);
+        throw EvalError(
+            ErrorInfo {
+                .hint = hintfmt("at least one argument to 'exec' required"),
+                .nixCode = NixCode { .errPos = pos }
+            });
     }
     PathSet context;
     auto program = state.coerceToString(pos, *elems[0], context, false, false);
@@ -217,22 +230,25 @@ void prim_exec(EvalState & state, const Pos & pos, Value * * args, Value & v)
     try {
         state.realiseContext(context);
     } catch (InvalidPathError & e) {
-        throw EvalError(format("cannot execute '%1%', since path '%2%' is not valid, at %3%")
-            % program % e.path % pos);
-    }
+        throw EvalError(
+            ErrorInfo {
+                .hint = hintfmt("cannot execute '%1%', since path '%2%' is not valid",
+                    program, e.path),
+                .nixCode = NixCode { .errPos = pos }
+            });}
 
     auto output = runProgram(program, true, commandArgs);
     Expr * parsed;
     try {
         parsed = state.parseExprFromString(output, pos.file);
     } catch (Error & e) {
-        e.addPrefix(format("While parsing the output from '%1%', at %2%\n") % program % pos);
+        e.addPrefix(fmt("While parsing the output from '%1%', at %2%\n", program, pos));
         throw;
     }
     try {
         state.eval(parsed, v);
     } catch (Error & e) {
-        e.addPrefix(format("While evaluating the output from '%1%', at %2%\n") % program % pos);
+        e.addPrefix(fmt("While evaluating the output from '%1%', at %2%\n", program, pos));
         throw;
     }
 }
@@ -338,7 +354,7 @@ struct CompareValues
         if (v1->type == tInt && v2->type == tFloat)
             return v1->integer < v2->fpoint;
         if (v1->type != v2->type)
-            throw EvalError(format("cannot compare %1% with %2%") % showType(*v1) % showType(*v2));
+            throw EvalError("cannot compare %1% with %2%", showType(*v1), showType(*v2));
         switch (v1->type) {
             case tInt:
                 return v1->integer < v2->integer;
@@ -349,7 +365,7 @@ struct CompareValues
             case tPath:
                 return strcmp(v1->path, v2->path) < 0;
             default:
-                throw EvalError(format("cannot compare %1% with %2%") % showType(*v1) % showType(*v2));
+                throw EvalError("cannot compare %1% with %2%", showType(*v1), showType(*v2));
         }
     }
 };
@@ -370,7 +386,11 @@ static void prim_genericClosure(EvalState & state, const Pos & pos, Value * * ar
     Bindings::iterator startSet =
         args[0]->attrs->find(state.symbols.create("startSet"));
     if (startSet == args[0]->attrs->end())
-        throw EvalError(format("attribute 'startSet' required, at %1%") % pos);
+        throw EvalError(
+            ErrorInfo {
+                .hint = hintfmt("attribute 'startSet' required"),
+                .nixCode = NixCode { .errPos = pos }
+            });
     state.forceList(*startSet->value, pos);
 
     ValueList workSet;
@@ -381,7 +401,11 @@ static void prim_genericClosure(EvalState & state, const Pos & pos, Value * * ar
     Bindings::iterator op =
         args[0]->attrs->find(state.symbols.create("operator"));
     if (op == args[0]->attrs->end())
-        throw EvalError(format("attribute 'operator' required, at %1%") % pos);
+        throw EvalError(
+            ErrorInfo {
+                .hint = hintfmt("attribute 'operator' required"),
+                .nixCode = NixCode { .errPos = pos }
+            });
     state.forceValue(*op->value, pos);
 
     /* Construct the closure by applying the operator to element of
@@ -400,7 +424,11 @@ static void prim_genericClosure(EvalState & state, const Pos & pos, Value * * ar
         Bindings::iterator key =
             e->attrs->find(state.symbols.create("key"));
         if (key == e->attrs->end())
-            throw EvalError(format("attribute 'key' required, at %1%") % pos);
+            throw EvalError(
+                ErrorInfo {
+                    .hint = hintfmt("attribute 'key' required"),
+                    .nixCode = NixCode { .errPos = pos }
+                });
         state.forceValue(*key->value, pos);
 
         if (!doneKeys.insert(key->value).second) continue;
@@ -430,7 +458,7 @@ static void prim_abort(EvalState & state, const Pos & pos, Value * * args, Value
 {
     PathSet context;
     string s = state.coerceToString(pos, *args[0], context);
-    throw Abort(format("evaluation aborted with the following error message: '%1%'") % s);
+    throw Abort("evaluation aborted with the following error message: '%1%'", s);
 }
 
 
@@ -505,9 +533,9 @@ static void prim_trace(EvalState & state, const Pos & pos, Value * * args, Value
 {
     state.forceValue(*args[0], pos);
     if (args[0]->type == tString)
-        printError(format("trace: %1%") % args[0]->string.s);
+        printError("trace: %1%", args[0]->string.s);
     else
-        printError(format("trace: %1%") % *args[0]);
+        printError("trace: %1%", *args[0]);
     state.forceValue(*args[1], pos);
     v = *args[1];
 }
@@ -532,13 +560,17 @@ static void prim_derivationStrict(EvalState & state, const Pos & pos, Value * *
     /* Figure out the name first (for stack backtraces). */
     Bindings::iterator attr = args[0]->attrs->find(state.sName);
     if (attr == args[0]->attrs->end())
-        throw EvalError(format("required attribute 'name' missing, at %1%") % pos);
+        throw EvalError(
+            ErrorInfo {
+                .hint = hintfmt("required attribute 'name' missing"),
+                .nixCode = NixCode { .errPos = pos }
+            });
     string drvName;
     Pos & posDrvName(*attr->pos);
     try {
         drvName = state.forceStringNoCtx(*attr->value, pos);
     } catch (Error & e) {
-        e.addPrefix(format("while evaluating the derivation attribute 'name' at %1%:\n") % posDrvName);
+        e.addPrefix(fmt("while evaluating the derivation attribute 'name' at %1%:\n", posDrvName));
         throw;
     }
 
@@ -575,25 +607,42 @@ static void prim_derivationStrict(EvalState & state, const Pos & pos, Value * *
         auto handleHashMode = [&](const std::string & s) {
             if (s == "recursive") ingestionMethod = FileIngestionMethod::Recursive;
             else if (s == "flat") ingestionMethod = FileIngestionMethod::Flat;
-            else throw EvalError("invalid value '%s' for 'outputHashMode' attribute, at %s", s, posDrvName);
+            else 
+                throw EvalError(
+                    ErrorInfo { 
+                        .hint = hintfmt("invalid value '%s' for 'outputHashMode' attribute", s),
+                        .nixCode = NixCode { .errPos = posDrvName }
+                    });
         };
 
         auto handleOutputs = [&](const Strings & ss) {
             outputs.clear();
             for (auto & j : ss) {
                 if (outputs.find(j) != outputs.end())
-                    throw EvalError(format("duplicate derivation output '%1%', at %2%") % j % posDrvName);
+                    throw EvalError(
+                        ErrorInfo { 
+                            .hint = hintfmt("duplicate derivation output '%1%'", j),
+                            .nixCode = NixCode { .errPos = posDrvName }
+                        });
                 /* !!! Check whether j is a valid attribute
                    name. */
                 /* Derivations cannot be named ‘drv’, because
                    then we'd have an attribute ‘drvPath’ in
                    the resulting set. */
                 if (j == "drv")
-                    throw EvalError(format("invalid derivation output name 'drv', at %1%") % posDrvName);
+                    throw EvalError(
+                        ErrorInfo { 
+                            .hint = hintfmt("invalid derivation output name 'drv'" ),
+                            .nixCode = NixCode { .errPos = posDrvName }
+                        });
                 outputs.insert(j);
             }
             if (outputs.empty())
-                throw EvalError(format("derivation cannot have an empty set of outputs, at %1%") % posDrvName);
+                throw EvalError(
+                    ErrorInfo { 
+                        .hint = hintfmt("derivation cannot have an empty set of outputs"),
+                        .nixCode = NixCode { .errPos = posDrvName }
+                    });
         };
 
         try {
@@ -705,18 +754,35 @@ static void prim_derivationStrict(EvalState & state, const Pos & pos, Value * *
 
     /* Do we have all required attributes? */
     if (drv.builder == "")
-        throw EvalError(format("required attribute 'builder' missing, at %1%") % posDrvName);
+        throw EvalError(
+            ErrorInfo { 
+                .hint = hintfmt("required attribute 'builder' missing"),
+                .nixCode = NixCode { .errPos = posDrvName }
+            });
+
     if (drv.platform == "")
-        throw EvalError(format("required attribute 'system' missing, at %1%") % posDrvName);
+        throw EvalError(
+            ErrorInfo { 
+                .hint = hintfmt("required attribute 'system' missing"),
+                .nixCode = NixCode { .errPos = posDrvName }
+            });
 
     /* Check whether the derivation name is valid. */
     if (isDerivation(drvName))
-        throw EvalError("derivation names are not allowed to end in '%s', at %s", drvExtension, posDrvName);
+        throw EvalError(
+            ErrorInfo { 
+                .hint = hintfmt("derivation names are not allowed to end in '%s'", drvExtension),
+                .nixCode = NixCode { .errPos = posDrvName }
+            });
 
     if (outputHash) {
         /* Handle fixed-output derivations. */
         if (outputs.size() != 1 || *(outputs.begin()) != "out")
-            throw Error(format("multiple outputs are not supported in fixed-output derivations, at %1%") % posDrvName);
+            throw Error(
+                ErrorInfo { 
+                    .hint = hintfmt("multiple outputs are not supported in fixed-output derivations"),
+                    .nixCode = NixCode { .errPos = posDrvName }
+                });
 
         HashType ht = outputHashAlgo.empty() ? htUnknown : parseHashType(outputHashAlgo);
 
@@ -821,7 +887,11 @@ static void prim_storePath(EvalState & state, const Pos & pos, Value * * args, V
        e.g. nix-push does the right thing. */
     if (!state.store->isStorePath(path)) path = canonPath(path, true);
     if (!state.store->isInStore(path))
-        throw EvalError(format("path '%1%' is not in the Nix store, at %2%") % path % pos);
+        throw EvalError(
+            ErrorInfo {
+                .hint = hintfmt("path '%1%' is not in the Nix store", path),
+                .nixCode = NixCode { .errPos = pos }
+            });
     Path path2 = state.store->toStorePath(path);
     if (!settings.readOnlyMode)
         state.store->ensurePath(state.store->parseStorePath(path2));
@@ -837,9 +907,13 @@ static void prim_pathExists(EvalState & state, const Pos & pos, Value * * args,
     try {
         state.realiseContext(context);
     } catch (InvalidPathError & e) {
-        throw EvalError(format(
-                "cannot check the existence of '%1%', since path '%2%' is not valid, at %3%")
-            % path % e.path % pos);
+        throw EvalError(
+            ErrorInfo {
+                .hint = hintfmt(
+                    "cannot check the existence of '%1%', since path '%2%' is not valid", 
+                    path, e.path),
+                .nixCode = NixCode { .errPos = pos }
+            });
     }
 
     try {
@@ -882,12 +956,16 @@ static void prim_readFile(EvalState & state, const Pos & pos, Value * * args, Va
     try {
         state.realiseContext(context);
     } catch (InvalidPathError & e) {
-        throw EvalError(format("cannot read '%1%', since path '%2%' is not valid, at %3%")
-            % path % e.path % pos);
-    }
+        throw EvalError(
+            ErrorInfo {
+                .hint = hintfmt("cannot read '%1%', since path '%2%' is not valid"
+                     , path, e.path),
+                .nixCode = NixCode { .errPos = pos }
+            });
+ }
     string s = readFile(state.checkSourcePath(state.toRealPath(path, context)));
     if (s.find((char) 0) != string::npos)
-        throw Error(format("the contents of the file '%1%' cannot be represented as a Nix string") % path);
+        throw Error("the contents of the file '%1%' cannot be represented as a Nix string", path);
     mkString(v, s.c_str());
 }
 
@@ -911,7 +989,11 @@ static void prim_findFile(EvalState & state, const Pos & pos, Value * * args, Va
 
         i = v2.attrs->find(state.symbols.create("path"));
         if (i == v2.attrs->end())
-            throw EvalError(format("attribute 'path' missing, at %1%") % pos);
+            throw EvalError(
+                ErrorInfo {
+                    .hint = hintfmt("attribute 'path' missing"),
+                    .nixCode = NixCode { .errPos = pos }
+                });
 
         PathSet context;
         string path = state.coerceToString(pos, *i->value, context, false, false);
@@ -919,8 +1001,12 @@ static void prim_findFile(EvalState & state, const Pos & pos, Value * * args, Va
         try {
             state.realiseContext(context);
         } catch (InvalidPathError & e) {
-            throw EvalError(format("cannot find '%1%', since path '%2%' is not valid, at %3%")
-                % path % e.path % pos);
+            throw EvalError(
+                ErrorInfo {
+                    .hint = hintfmt("cannot find '%1%', since path '%2%' is not valid",
+                        path, e.path),
+                    .nixCode = NixCode { .errPos = pos }
+                });
         }
 
         searchPath.emplace_back(prefix, path);
@@ -937,7 +1023,11 @@ static void prim_hashFile(EvalState & state, const Pos & pos, Value * * args, Va
     string type = state.forceStringNoCtx(*args[0], pos);
     HashType ht = parseHashType(type);
     if (ht == htUnknown)
-      throw Error(format("unknown hash type '%1%', at %2%") % type % pos);
+      throw Error(
+          ErrorInfo {
+              .hint = hintfmt("unknown hash type '%1%'", type),
+              .nixCode = NixCode { .errPos = pos }
+          });
 
     PathSet context; // discarded
     Path p = state.coerceToPath(pos, *args[1], context);
@@ -953,8 +1043,12 @@ static void prim_readDir(EvalState & state, const Pos & pos, Value * * args, Val
     try {
         state.realiseContext(ctx);
     } catch (InvalidPathError & e) {
-        throw EvalError(format("cannot read '%1%', since path '%2%' is not valid, at %3%")
-            % path % e.path % pos);
+        throw EvalError(
+            ErrorInfo {
+                .hint = hintfmt("cannot read '%1%', since path '%2%' is not valid",
+                        path, e.path),
+                .nixCode = NixCode { .errPos = pos }
+            });
     }
 
     DirEntries entries = readDirectory(state.checkSourcePath(path));
@@ -1024,9 +1118,15 @@ static void prim_toFile(EvalState & state, const Pos & pos, Value * * args, Valu
 
     for (auto path : context) {
         if (path.at(0) != '/')
-            throw EvalError(format(
-                "in 'toFile': the file named '%1%' must not contain a reference "
-                "to a derivation but contains (%2%), at %3%") % name % path % pos);
+            throw EvalError(
+                ErrorInfo { 
+                    .hint = hintfmt(
+                        "in 'toFile': the file named '%1%' must not contain a reference "
+                        "to a derivation but contains (%2%)",
+                        name,
+                        path),
+                    .nixCode = NixCode { .errPos = pos }
+                });
         refs.insert(state.store->parseStorePath(path));
     }
 
@@ -1094,11 +1194,21 @@ static void prim_filterSource(EvalState & state, const Pos & pos, Value * * args
     PathSet context;
     Path path = state.coerceToPath(pos, *args[1], context);
     if (!context.empty())
-        throw EvalError(format("string '%1%' cannot refer to other paths, at %2%") % path % pos);
+        throw EvalError(
+            ErrorInfo { 
+                .hint = hintfmt("string '%1%' cannot refer to other paths", path),
+                .nixCode = NixCode { .errPos = pos }
+            });
 
     state.forceValue(*args[0], pos);
     if (args[0]->type != tLambda)
-        throw TypeError(format("first argument in call to 'filterSource' is not a function but %1%, at %2%") % showType(*args[0]) % pos);
+        throw TypeError(
+            ErrorInfo { 
+                .hint = hintfmt(
+                    "first argument in call to 'filterSource' is not a function but %1%", 
+                    showType(*args[0])),
+                .nixCode = NixCode { .errPos = pos }
+            });
 
     addPath(state, pos, std::string(baseNameOf(path)), path, args[0], FileIngestionMethod::Recursive, Hash(), v);
 }
@@ -1118,7 +1228,12 @@ static void prim_path(EvalState & state, const Pos & pos, Value * * args, Value
             PathSet context;
             path = state.coerceToPath(*attr.pos, *attr.value, context);
             if (!context.empty())
-                throw EvalError(format("string '%1%' cannot refer to other paths, at %2%") % path % *attr.pos);
+                throw EvalError(
+                    ErrorInfo { 
+                        .hint = hintfmt("string '%1%' cannot refer to other paths", 
+                            path),
+                        .nixCode = NixCode { .errPos = *attr.pos }
+                    });
         } else if (attr.name == state.sName)
             name = state.forceStringNoCtx(*attr.value, *attr.pos);
         else if (n == "filter") {
@@ -1129,10 +1244,19 @@ static void prim_path(EvalState & state, const Pos & pos, Value * * args, Value
         else if (n == "sha256")
             expectedHash = newHashAllowEmpty(state.forceStringNoCtx(*attr.value, *attr.pos), htSHA256);
         else
-            throw EvalError(format("unsupported argument '%1%' to 'addPath', at %2%") % attr.name % *attr.pos);
+            throw EvalError(
+                ErrorInfo { 
+                    .hint = hintfmt("unsupported argument '%1%' to 'addPath'",
+                        attr.name),
+                    .nixCode = NixCode { .errPos = *attr.pos }
+                });
     }
     if (path.empty())
-        throw EvalError(format("'path' required, at %1%") % pos);
+        throw EvalError(
+            ErrorInfo { 
+                .hint = hintfmt("'path' required"),
+                .nixCode = NixCode { .errPos = pos }
+            });
     if (name.empty())
         name = baseNameOf(path);
 
@@ -1190,7 +1314,11 @@ void prim_getAttr(EvalState & state, const Pos & pos, Value * * args, Value & v)
     // !!! Should we create a symbol here or just do a lookup?
     Bindings::iterator i = args[1]->attrs->find(state.symbols.create(attr));
     if (i == args[1]->attrs->end())
-        throw EvalError(format("attribute '%1%' missing, at %2%") % attr % pos);
+        throw EvalError(
+            ErrorInfo { 
+                .hint = hintfmt("attribute '%1%' missing", attr),
+                .nixCode = NixCode { .errPos = pos }
+            });
     // !!! add to stack trace?
     if (state.countCalls && i->pos) state.attrSelects[*i->pos]++;
     state.forceValue(*i->value, pos);
@@ -1270,15 +1398,22 @@ static void prim_listToAttrs(EvalState & state, const Pos & pos, Value * * args,
 
         Bindings::iterator j = v2.attrs->find(state.sName);
         if (j == v2.attrs->end())
-            throw TypeError(format("'name' attribute missing in a call to 'listToAttrs', at %1%") % pos);
+            throw TypeError(
+                ErrorInfo { 
+                    .hint = hintfmt("'name' attribute missing in a call to 'listToAttrs'"), 
+                    .nixCode = NixCode { .errPos = pos }
+                });
         string name = state.forceStringNoCtx(*j->value, pos);
 
         Symbol sym = state.symbols.create(name);
         if (seen.insert(sym).second) {
             Bindings::iterator j2 = v2.attrs->find(state.symbols.create(state.sValue));
             if (j2 == v2.attrs->end())
-                throw TypeError(format("'value' attribute missing in a call to 'listToAttrs', at %1%") % pos);
-
+                throw TypeError(
+                    ErrorInfo { 
+                        .hint = hintfmt("'value' attribute missing in a call to 'listToAttrs'"), 
+                        .nixCode = NixCode { .errPos = pos }
+                    });
             v.attrs->push_back(Attr(sym, j2->value, j2->pos));
         }
     }
@@ -1351,7 +1486,11 @@ static void prim_functionArgs(EvalState & state, const Pos & pos, Value * * args
 {
     state.forceValue(*args[0], pos);
     if (args[0]->type != tLambda)
-        throw TypeError(format("'functionArgs' requires a function, at %1%") % pos);
+        throw TypeError(
+            ErrorInfo { 
+                .hint = hintfmt("'functionArgs' requires a function"), 
+                .nixCode = NixCode { .errPos = pos }
+            });
 
     if (!args[0]->lambda.fun->matchAttrs) {
         state.mkAttrs(v, 0);
@@ -1404,7 +1543,11 @@ static void elemAt(EvalState & state, const Pos & pos, Value & list, int n, Valu
 {
     state.forceList(list, pos);
     if (n < 0 || (unsigned int) n >= list.listSize())
-        throw Error(format("list index %1% is out of bounds, at %2%") % n % pos);
+        throw Error(
+            ErrorInfo { 
+                .hint = hintfmt("list index %1% is out of bounds", n), 
+                .nixCode = NixCode { .errPos = pos }
+            });
     state.forceValue(*list.listElems()[n], pos);
     v = *list.listElems()[n];
 }
@@ -1431,7 +1574,12 @@ static void prim_tail(EvalState & state, const Pos & pos, Value * * args, Value
 {
     state.forceList(*args[0], pos);
     if (args[0]->listSize() == 0)
-        throw Error(format("'tail' called on an empty list, at %1%") % pos);
+        throw Error(
+            ErrorInfo { 
+                .hint = hintfmt("'tail' called on an empty list"), 
+                .nixCode = NixCode { .errPos = pos }
+            });
+
     state.mkList(v, args[0]->listSize() - 1);
     for (unsigned int n = 0; n < v.listSize(); ++n)
         v.listElems()[n] = args[0]->listElems()[n + 1];
@@ -1572,7 +1720,12 @@ static void prim_genList(EvalState & state, const Pos & pos, Value * * args, Val
     auto len = state.forceInt(*args[1], pos);
 
     if (len < 0)
-        throw EvalError(format("cannot create list of size %1%, at %2%") % len % pos);
+        throw EvalError(
+            ErrorInfo { 
+                .hint = hintfmt("cannot create list of size %1%", len), 
+                .nixCode = NixCode { .errPos = pos }
+            });
+
 
     state.mkList(v, len);
 
@@ -1730,7 +1883,12 @@ static void prim_div(EvalState & state, const Pos & pos, Value * * args, Value &
     state.forceValue(*args[1], pos);
 
     NixFloat f2 = state.forceFloat(*args[1], pos);
-    if (f2 == 0) throw EvalError(format("division by zero, at %1%") % pos);
+    if (f2 == 0) 
+        throw EvalError(
+            ErrorInfo { 
+                .hint = hintfmt("division by zero"), 
+                .nixCode = NixCode { .errPos = pos }
+            });
 
     if (args[0]->type == tFloat || args[1]->type == tFloat) {
         mkFloat(v, state.forceFloat(*args[0], pos) / state.forceFloat(*args[1], pos));
@@ -1739,7 +1897,12 @@ static void prim_div(EvalState & state, const Pos & pos, Value * * args, Value &
         NixInt i2 = state.forceInt(*args[1], pos);
         /* Avoid division overflow as it might raise SIGFPE. */
         if (i1 == std::numeric_limits<NixInt>::min() && i2 == -1)
-            throw EvalError(format("overflow in integer division, at %1%") % pos);
+            throw EvalError(
+                ErrorInfo { 
+                    .hint = hintfmt("overflow in integer division"), 
+                    .nixCode = NixCode { .errPos = pos }
+                });
+
         mkInt(v, i1 / i2);
     }
 }
@@ -1795,7 +1958,12 @@ static void prim_substring(EvalState & state, const Pos & pos, Value * * args, V
     PathSet context;
     string s = state.coerceToString(pos, *args[2], context);
 
-    if (start < 0) throw EvalError(format("negative start position in 'substring', at %1%") % pos);
+    if (start < 0) 
+        throw EvalError(
+            ErrorInfo { 
+                .hint = hintfmt("negative start position in 'substring'"), 
+                .nixCode = NixCode { .errPos = pos }
+            });
 
     mkString(v, (unsigned int) start >= s.size() ? "" : string(s, start, len), context);
 }
@@ -1815,7 +1983,11 @@ static void prim_hashString(EvalState & state, const Pos & pos, Value * * args,
     string type = state.forceStringNoCtx(*args[0], pos);
     HashType ht = parseHashType(type);
     if (ht == htUnknown)
-      throw Error(format("unknown hash type '%1%', at %2%") % type % pos);
+        throw Error(
+            ErrorInfo { 
+                .hint = hintfmt("unknown hash type '%1%'", type), 
+                .nixCode = NixCode { .errPos = pos }
+            });
 
     PathSet context; // discarded
     string s = state.forceString(*args[1], context, pos);
@@ -1857,10 +2029,18 @@ void prim_match(EvalState & state, const Pos & pos, Value * * args, Value & v)
 
     } catch (std::regex_error &e) {
         if (e.code() == std::regex_constants::error_space) {
-          // limit is _GLIBCXX_REGEX_STATE_LIMIT for libstdc++
-          throw EvalError("memory limit exceeded by regular expression '%s', at %s", re, pos);
+            // limit is _GLIBCXX_REGEX_STATE_LIMIT for libstdc++
+            throw EvalError(
+                ErrorInfo {
+                    .hint = hintfmt("memory limit exceeded by regular expression '%s'", re),
+                    .nixCode = NixCode { .errPos = pos }
+                });
         } else {
-          throw EvalError("invalid regular expression '%s', at %s", re, pos);
+            throw EvalError(
+                ErrorInfo {
+                    .hint = hintfmt("invalid regular expression '%s'", re),
+                    .nixCode = NixCode { .errPos = pos }
+                });
         }
     }
 }
@@ -1924,10 +2104,18 @@ static void prim_split(EvalState & state, const Pos & pos, Value * * args, Value
 
     } catch (std::regex_error &e) {
         if (e.code() == std::regex_constants::error_space) {
-          // limit is _GLIBCXX_REGEX_STATE_LIMIT for libstdc++
-          throw EvalError("memory limit exceeded by regular expression '%s', at %s", re, pos);
+            // limit is _GLIBCXX_REGEX_STATE_LIMIT for libstdc++
+            throw EvalError(
+                ErrorInfo {
+                    .hint = hintfmt("memory limit exceeded by regular expression '%s'", re),
+                    .nixCode = NixCode { .errPos = pos }
+                });
         } else {
-          throw EvalError("invalid regular expression '%s', at %s", re, pos);
+            throw EvalError(
+                ErrorInfo {
+                    .hint = hintfmt("invalid regular expression '%s'", re),
+                    .nixCode = NixCode { .errPos = pos }
+                });
         }
     }
 }
@@ -1958,7 +2146,11 @@ static void prim_replaceStrings(EvalState & state, const Pos & pos, Value * * ar
     state.forceList(*args[0], pos);
     state.forceList(*args[1], pos);
     if (args[0]->listSize() != args[1]->listSize())
-        throw EvalError(format("'from' and 'to' arguments to 'replaceStrings' have different lengths, at %1%") % pos);
+        throw EvalError(
+            ErrorInfo { 
+                .hint = hintfmt("'from' and 'to' arguments to 'replaceStrings' have different lengths"), 
+                .nixCode = NixCode { .errPos = pos }
+            });
 
     vector<string> from;
     from.reserve(args[0]->listSize());
diff --git a/src/libexpr/primops/context.cc b/src/libexpr/primops/context.cc
index 94fa0158c80eb21017b2e347a10e7b7a9a22e57c..7f895fc01594f399058391995bf78fa495a29830 100644
--- a/src/libexpr/primops/context.cc
+++ b/src/libexpr/primops/context.cc
@@ -146,7 +146,11 @@ static void prim_appendContext(EvalState & state, const Pos & pos, Value * * arg
     auto sAllOutputs = state.symbols.create("allOutputs");
     for (auto & i : *args[1]->attrs) {
         if (!state.store->isStorePath(i.name))
-            throw EvalError("Context key '%s' is not a store path, at %s", i.name, i.pos);
+            throw EvalError(
+                ErrorInfo { 
+                    .hint = hintfmt("Context key '%s' is not a store path", i.name),
+                    .nixCode = NixCode { .errPos = *i.pos }
+                });
         if (!settings.readOnlyMode)
             state.store->ensurePath(state.store->parseStorePath(i.name));
         state.forceAttrs(*i.value, *i.pos);
@@ -160,7 +164,11 @@ static void prim_appendContext(EvalState & state, const Pos & pos, Value * * arg
         if (iter != i.value->attrs->end()) {
             if (state.forceBool(*iter->value, *iter->pos)) {
                 if (!isDerivation(i.name)) {
-                    throw EvalError("Tried to add all-outputs context of %s, which is not a derivation, to a string, at %s", i.name, i.pos);
+                    throw EvalError(
+                        ErrorInfo { 
+                            .hint = hintfmt("Tried to add all-outputs context of %s, which is not a derivation, to a string", i.name),
+                            .nixCode = NixCode { .errPos = *i.pos }
+                        });
                 }
                 context.insert("=" + string(i.name));
             }
@@ -170,7 +178,11 @@ static void prim_appendContext(EvalState & state, const Pos & pos, Value * * arg
         if (iter != i.value->attrs->end()) {
             state.forceList(*iter->value, *iter->pos);
             if (iter->value->listSize() && !isDerivation(i.name)) {
-                throw EvalError("Tried to add derivation output context of %s, which is not a derivation, to a string, at %s", i.name, i.pos);
+                throw EvalError(
+                    ErrorInfo { 
+                        .hint = hintfmt("Tried to add derivation output context of %s, which is not a derivation, to a string", i.name),
+                        .nixCode = NixCode { .errPos = *i.pos }
+                    });
             }
             for (unsigned int n = 0; n < iter->value->listSize(); ++n) {
                 auto name = state.forceStringNoCtx(*iter->value->listElems()[n], *iter->pos);
diff --git a/src/libexpr/primops/fetchGit.cc b/src/libexpr/primops/fetchGit.cc
index 1a8798fccb0e88470ddc7ed0f0913ee27d9f5533..52826b56ccf7e4140d2fe9873e1486a416eb5d2e 100644
--- a/src/libexpr/primops/fetchGit.cc
+++ b/src/libexpr/primops/fetchGit.cc
@@ -35,11 +35,19 @@ static void prim_fetchGit(EvalState & state, const Pos & pos, Value * * args, Va
             else if (n == "submodules")
                 fetchSubmodules = state.forceBool(*attr.value, *attr.pos);
             else
-                throw EvalError("unsupported argument '%s' to 'fetchGit', at %s", attr.name, *attr.pos);
+                throw EvalError(
+                    ErrorInfo { 
+                        .hint = hintfmt("unsupported argument '%s' to 'fetchGit'", attr.name),
+                        .nixCode = NixCode { .errPos = *attr.pos }
+                    });
         }
 
         if (url.empty())
-            throw EvalError(format("'url' argument required, at %1%") % pos);
+            throw EvalError(
+                ErrorInfo { 
+                    .hint = hintfmt("'url' argument required"),
+                    .nixCode = NixCode { .errPos = pos }
+                });
 
     } else
         url = state.coerceToString(pos, *args[0], context, false, false);
diff --git a/src/libexpr/primops/fetchMercurial.cc b/src/libexpr/primops/fetchMercurial.cc
index 0a1ba49d5e42caf70aa34dd81d7b972caca724f1..bb008ba6b366ccfec75de22cbdb2857a772ff777 100644
--- a/src/libexpr/primops/fetchMercurial.cc
+++ b/src/libexpr/primops/fetchMercurial.cc
@@ -38,11 +38,19 @@ static void prim_fetchMercurial(EvalState & state, const Pos & pos, Value * * ar
             else if (n == "name")
                 name = state.forceStringNoCtx(*attr.value, *attr.pos);
             else
-                throw EvalError("unsupported argument '%s' to 'fetchMercurial', at %s", attr.name, *attr.pos);
+                throw EvalError(
+                    ErrorInfo { 
+                        .hint = hintfmt("unsupported argument '%s' to 'fetchMercurial'", attr.name),
+                        .nixCode = NixCode { .errPos = *attr.pos }
+                    });
         }
 
         if (url.empty())
-            throw EvalError(format("'url' argument required, at %1%") % pos);
+            throw EvalError(
+                ErrorInfo { 
+                    .hint = hintfmt("'url' argument required"),
+                    .nixCode = NixCode { .errPos = pos }
+                });
 
     } else
         url = state.coerceToString(pos, *args[0], context, false, false);
diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc
index 97254aa044aaf5d46a57dbea90c0d3563a46c600..62bc4f4336fde295d99ef0c4ef933906691c7277 100644
--- a/src/libexpr/primops/fetchTree.cc
+++ b/src/libexpr/primops/fetchTree.cc
@@ -66,7 +66,11 @@ static void prim_fetchTree(EvalState & state, const Pos & pos, Value * * args, V
         }
 
         if (!attrs.count("type"))
-            throw Error("attribute 'type' is missing in call to 'fetchTree', at %s", pos);
+            throw Error(
+                ErrorInfo {
+                    .hint = hintfmt("attribute 'type' is missing in call to 'fetchTree'"),
+                    .nixCode = NixCode { .errPos = pos }
+                    });
 
         input = fetchers::inputFromAttrs(attrs);
     } else
@@ -107,13 +111,20 @@ static void fetch(EvalState & state, const Pos & pos, Value * * args, Value & v,
             else if (n == "name")
                 name = state.forceStringNoCtx(*attr.value, *attr.pos);
             else
-                throw EvalError("unsupported argument '%s' to '%s', at %s",
-                    attr.name, who, *attr.pos);
-        }
+                throw EvalError(
+                    ErrorInfo {
+                        .hint = hintfmt("unsupported argument '%s' to '%s'",
+                            attr.name, who),
+                        .nixCode = NixCode { .errPos = *attr.pos }
+                    });
+            }
 
         if (!url)
-            throw EvalError("'url' argument required, at %s", pos);
-
+            throw EvalError(
+                ErrorInfo {
+                    .hint = hintfmt("'url' argument required"),
+                    .nixCode = NixCode { .errPos = pos }
+                });
     } else
         url = state.forceStringNoCtx(*args[0], pos);
 
diff --git a/src/libexpr/primops/fromTOML.cc b/src/libexpr/primops/fromTOML.cc
index c43324dbb51a3a570de6e6de93dc8dcb7a715acf..9480694013046e7c07af66d90f5e66045fae70fa 100644
--- a/src/libexpr/primops/fromTOML.cc
+++ b/src/libexpr/primops/fromTOML.cc
@@ -81,7 +81,11 @@ static void prim_fromTOML(EvalState & state, const Pos & pos, Value * * args, Va
     try {
         visit(v, parser(tomlStream).parse());
     } catch (std::runtime_error & e) {
-        throw EvalError("while parsing a TOML string at %s: %s", pos, e.what());
+        throw EvalError(
+            ErrorInfo {
+                .hint = hintfmt("while parsing a TOML string: %s", e.what()),
+                .nixCode = NixCode { .errPos = pos }
+            });
     }
 }
 
diff --git a/src/libexpr/value-to-json.cc b/src/libexpr/value-to-json.cc
index 5fe8570adeb48c17d3283587922c64a2b52a88da..6ec8315bab0e31463bc74b019973982ef0dc472b 100644
--- a/src/libexpr/value-to-json.cc
+++ b/src/libexpr/value-to-json.cc
@@ -79,7 +79,7 @@ void printValueAsJSON(EvalState & state, bool strict,
             break;
 
         default:
-            throw TypeError(format("cannot convert %1% to JSON") % showType(v));
+            throw TypeError("cannot convert %1% to JSON", showType(v));
     }
 }
 
@@ -93,7 +93,7 @@ void printValueAsJSON(EvalState & state, bool strict,
 void ExternalValueBase::printValueAsJSON(EvalState & state, bool strict,
     JSONPlaceholder & out, PathSet & context) const
 {
-    throw TypeError(format("cannot convert %1% to JSON") % showType());
+    throw TypeError("cannot convert %1% to JSON", showType());
 }
 
 
diff --git a/src/libmain/progress-bar.cc b/src/libmain/progress-bar.cc
index b287de8a3bcdeb17c549e5486dd5e14e08181ccf..20d9915a089698e255df7c69ef2acf9adf4721c5 100644
--- a/src/libmain/progress-bar.cc
+++ b/src/libmain/progress-bar.cc
@@ -129,6 +129,16 @@ public:
         log(*state, lvl, fs.s);
     }
 
+    void logEI(const ErrorInfo &ei) override
+    {
+        auto state(state_.lock());
+
+        std::stringstream oss; 
+        oss << ei;
+
+        log(*state, ei.level, oss.str());
+    }
+
     void log(State & state, Verbosity lvl, const std::string & s)
     {
         if (state.active) {
diff --git a/src/libmain/shared.cc b/src/libmain/shared.cc
index 3bbb5cf9380ef4d0cdf75c4e1778a580bd143be7..0f2c189a6dec3650293932969148d98eb7513872 100644
--- a/src/libmain/shared.cc
+++ b/src/libmain/shared.cc
@@ -76,7 +76,7 @@ string getArg(const string & opt,
     Strings::iterator & i, const Strings::iterator & end)
 {
     ++i;
-    if (i == end) throw UsageError(format("'%1%' requires an argument") % opt);
+    if (i == end) throw UsageError("'%1%' requires an argument", opt);
     return *i;
 }
 
@@ -235,7 +235,7 @@ bool LegacyArgs::processArgs(const Strings & args, bool finish)
     Strings ss(args);
     auto pos = ss.begin();
     if (!parseArg(pos, ss.end()))
-        throw UsageError(format("unexpected argument '%1%'") % args.front());
+        throw UsageError("unexpected argument '%1%'", args.front());
     return true;
 }
 
@@ -282,7 +282,7 @@ void showManPage(const string & name)
     restoreSignals();
     setenv("MANPATH", settings.nixManDir.c_str(), 1);
     execlp("man", "man", name.c_str(), nullptr);
-    throw SysError(format("command 'man %1%' failed") % name.c_str());
+    throw SysError("command 'man %1%' failed", name.c_str());
 }
 
 
@@ -290,6 +290,8 @@ int handleExceptions(const string & programName, std::function<void()> fun)
 {
     ReceiveInterrupts receiveInterrupts; // FIXME: need better place for this
 
+    ErrorInfo::programName = programName;
+
     string error = ANSI_RED "error:" ANSI_NORMAL " ";
     try {
         try {
@@ -305,12 +307,13 @@ int handleExceptions(const string & programName, std::function<void()> fun)
     } catch (Exit & e) {
         return e.status;
     } catch (UsageError & e) {
-        printError(
-            format(error + "%1%\nTry '%2% --help' for more information.")
-            % e.what() % programName);
+        logError(e.info());
+        printError("Try '%1% --help' for more information.", programName);
         return 1;
     } catch (BaseError & e) {
-        printError(format(error + "%1%%2%") % (settings.showTrace ? e.prefix() : "") % e.msg());
+        if (settings.showTrace && e.prefix() != "")
+            printError(e.prefix());
+        logError(e.info());
         if (e.prefix() != "" && !settings.showTrace)
             printError("(use '--show-trace' to show detailed location information)");
         return e.status;
@@ -347,7 +350,7 @@ RunPager::RunPager()
         execlp("pager", "pager", nullptr);
         execlp("less", "less", nullptr);
         execlp("more", "more", nullptr);
-        throw SysError(format("executing '%1%'") % pager);
+        throw SysError("executing '%1%'", pager);
     });
 
     pid.setKillSignal(SIGINT);
diff --git a/src/libmain/shared.hh b/src/libmain/shared.hh
index b49574652271451de42ecd46cbab111f3bde6a8f..f558247c0df6b386d396d7e210ca73bcfc8fcdf1 100644
--- a/src/libmain/shared.hh
+++ b/src/libmain/shared.hh
@@ -56,7 +56,7 @@ template<class N> N getIntArg(const string & opt,
     Strings::iterator & i, const Strings::iterator & end, bool allowUnit)
 {
     ++i;
-    if (i == end) throw UsageError(format("'%1%' requires an argument") % opt);
+    if (i == end) throw UsageError("'%1%' requires an argument", opt);
     string s = *i;
     N multiplier = 1;
     if (allowUnit && !s.empty()) {
@@ -66,13 +66,13 @@ template<class N> N getIntArg(const string & opt,
             else if (u == 'M') multiplier = 1ULL << 20;
             else if (u == 'G') multiplier = 1ULL << 30;
             else if (u == 'T') multiplier = 1ULL << 40;
-            else throw UsageError(format("invalid unit specifier '%1%'") % u);
+            else throw UsageError("invalid unit specifier '%1%'", u);
             s.resize(s.size() - 1);
         }
     }
     N n;
     if (!string2Int(s, n))
-        throw UsageError(format("'%1%' requires an integer argument") % opt);
+        throw UsageError("'%1%' requires an integer argument", opt);
     return n * multiplier;
 }
 
diff --git a/src/libmain/stack.cc b/src/libmain/stack.cc
index e6224de7d28f10e820fb3082fff03d154a3a5aca..b0a4a4c5dbe40e38f375b43630170724f834ff54 100644
--- a/src/libmain/stack.cc
+++ b/src/libmain/stack.cc
@@ -1,4 +1,4 @@
-#include "types.hh"
+#include "error.hh"
 
 #include <cstring>
 #include <cstddef>
diff --git a/src/libstore/binary-cache-store.cc b/src/libstore/binary-cache-store.cc
index 357962007a46688e70f99af398eba1de57b5b7b4..64933149560dd52f1074b8fee382d7cbbd4126db 100644
--- a/src/libstore/binary-cache-store.cc
+++ b/src/libstore/binary-cache-store.cc
@@ -40,14 +40,14 @@ void BinaryCacheStore::init()
         upsertFile(cacheInfoFile, "StoreDir: " + storeDir + "\n", "text/x-nix-cache-info");
     } else {
         for (auto & line : tokenizeString<Strings>(*cacheInfo, "\n")) {
-            size_t colon = line.find(':');
-            if (colon == std::string::npos) continue;
+            size_t colon= line.find(':');
+            if (colon ==std::string::npos) continue;
             auto name = line.substr(0, colon);
             auto value = trim(line.substr(colon + 1, std::string::npos));
             if (name == "StoreDir") {
                 if (value != storeDir)
-                    throw Error(format("binary cache '%s' is for Nix stores with prefix '%s', not '%s'")
-                        % getUri() % value % storeDir);
+                    throw Error("binary cache '%s' is for Nix stores with prefix '%s', not '%s'",
+                        getUri(), value, storeDir);
             } else if (name == "WantMassQuery") {
                 wantMassQuery.setDefault(value == "1" ? "true" : "false");
             } else if (name == "Priority") {
@@ -287,7 +287,7 @@ void BinaryCacheStore::narFromPath(const StorePath & storePath, Sink & sink)
     try {
         getFile(info->url, *decompressor);
     } catch (NoSuchBinaryCacheFile & e) {
-        throw SubstituteGone(e.what());
+        throw SubstituteGone(e.info());
     }
 
     decompressor->finish();
diff --git a/src/libstore/build.cc b/src/libstore/build.cc
index 3ab6220e3156cdf119e55dade1b7187accf94325..de393e837fa8dc8371c5d2823f19674b98835c93 100644
--- a/src/libstore/build.cc
+++ b/src/libstore/build.cc
@@ -453,7 +453,7 @@ static void commonChildInit(Pipe & logPipe)
        that e.g. ssh cannot open /dev/tty) and it doesn't receive
        terminal signals. */
     if (setsid() == -1)
-        throw SysError(format("creating a new session"));
+        throw SysError("creating a new session");
 
     /* Dup the write side of the logger pipe into stderr. */
     if (dup2(logPipe.writeSide.get(), STDERR_FILENO) == -1)
@@ -466,7 +466,7 @@ static void commonChildInit(Pipe & logPipe)
     /* Reroute stdin to /dev/null. */
     int fdDevNull = open(pathNullDevice.c_str(), O_RDWR);
     if (fdDevNull == -1)
-        throw SysError(format("cannot open '%1%'") % pathNullDevice);
+        throw SysError("cannot open '%1%'", pathNullDevice);
     if (dup2(fdDevNull, STDIN_FILENO) == -1)
         throw SysError("cannot dup null device into stdin");
     close(fdDevNull);
@@ -488,12 +488,18 @@ void handleDiffHook(
 
             auto diffRes = runProgram(diffHookOptions);
             if (!statusOk(diffRes.first))
-                throw ExecError(diffRes.first, fmt("diff-hook program '%1%' %2%", diffHook, statusToString(diffRes.first)));
+                throw ExecError(diffRes.first,
+                    "diff-hook program '%1%' %2%",
+                    diffHook,
+                    statusToString(diffRes.first));
 
             if (diffRes.second != "")
                 printError(chomp(diffRes.second));
         } catch (Error & error) {
-            printError("diff hook execution failed: %s", error.what());
+            ErrorInfo ei = error.info();
+            ei.hint = hintfmt("diff hook execution failed: %s",
+                (error.info().hint.has_value() ? error.info().hint->str() : ""));
+            logError(ei);
         }
     }
 }
@@ -542,37 +548,37 @@ bool UserLock::findFreeUser() {
     /* Get the members of the build-users-group. */
     struct group * gr = getgrnam(settings.buildUsersGroup.get().c_str());
     if (!gr)
-        throw Error(format("the group '%1%' specified in 'build-users-group' does not exist")
-            % settings.buildUsersGroup);
+        throw Error("the group '%1%' specified in 'build-users-group' does not exist",
+            settings.buildUsersGroup);
     gid = gr->gr_gid;
 
     /* Copy the result of getgrnam. */
     Strings users;
     for (char * * p = gr->gr_mem; *p; ++p) {
-        debug(format("found build user '%1%'") % *p);
+        debug("found build user '%1%'", *p);
         users.push_back(*p);
     }
 
     if (users.empty())
-        throw Error(format("the build users group '%1%' has no members")
-            % settings.buildUsersGroup);
+        throw Error("the build users group '%1%' has no members",
+            settings.buildUsersGroup);
 
     /* Find a user account that isn't currently in use for another
        build. */
     for (auto & i : users) {
-        debug(format("trying user '%1%'") % i);
+        debug("trying user '%1%'", i);
 
         struct passwd * pw = getpwnam(i.c_str());
         if (!pw)
-            throw Error(format("the user '%1%' in the group '%2%' does not exist")
-                % i % settings.buildUsersGroup);
+            throw Error("the user '%1%' in the group '%2%' does not exist",
+                i, settings.buildUsersGroup);
 
 
         fnUserLock = (format("%1%/userpool/%2%") % settings.nixStateDir % pw->pw_uid).str();
 
         AutoCloseFD fd = open(fnUserLock.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600);
         if (!fd)
-            throw SysError(format("opening user lock '%1%'") % fnUserLock);
+            throw SysError("opening user lock '%1%'", fnUserLock);
 
         if (lockFile(fd.get(), ltWrite, false)) {
             fdUserLock = std::move(fd);
@@ -581,8 +587,8 @@ bool UserLock::findFreeUser() {
 
             /* Sanity check... */
             if (uid == getuid() || uid == geteuid())
-                throw Error(format("the Nix user should not be a member of '%1%'")
-                    % settings.buildUsersGroup);
+                throw Error("the Nix user should not be a member of '%1%'",
+                    settings.buildUsersGroup);
 
 #if __linux__
             /* Get the list of supplementary groups of this build user.  This
@@ -592,7 +598,7 @@ bool UserLock::findFreeUser() {
             int err = getgrouplist(pw->pw_name, pw->pw_gid,
                 supplementaryGIDs.data(), &ngroups);
             if (err == -1)
-                throw Error(format("failed to get list of supplementary groups for '%1%'") % pw->pw_name);
+                throw Error("failed to get list of supplementary groups for '%1%'", pw->pw_name);
 
             supplementaryGIDs.resize(ngroups);
 #endif
@@ -601,6 +607,7 @@ bool UserLock::findFreeUser() {
             return true;
         }
     }
+
     return false;
 }
 
@@ -1151,7 +1158,10 @@ void DerivationGoal::loadDerivation()
     trace("loading derivation");
 
     if (nrFailed != 0) {
-        printError("cannot build missing derivation '%s'", worker.store.printStorePath(drvPath));
+        logError({
+            .name = "missing derivation during build",
+            .hint = hintfmt("cannot build missing derivation '%s'", worker.store.printStorePath(drvPath))
+        });
         done(BuildResult::MiscFailure);
         return;
     }
@@ -1302,8 +1312,12 @@ void DerivationGoal::repairClosure()
     /* Check each path (slow!). */
     for (auto & i : outputClosure) {
         if (worker.pathContentsGood(i)) continue;
-        printError("found corrupted or missing path '%s' in the output closure of '%s'",
-            worker.store.printStorePath(i), worker.store.printStorePath(drvPath));
+        logError({
+            .name = "Corrupt path in closure",
+            .hint = hintfmt(
+                "found corrupted or missing path '%s' in the output closure of '%s'",
+                worker.store.printStorePath(i), worker.store.printStorePath(drvPath))
+        });
         auto drvPath2 = outputsToDrv.find(i);
         if (drvPath2 == outputsToDrv.end())
             addWaitee(worker.makeSubstitutionGoal(i, Repair));
@@ -1337,8 +1351,12 @@ void DerivationGoal::inputsRealised()
     if (nrFailed != 0) {
         if (!useDerivation)
             throw Error("some dependencies of '%s' are missing", worker.store.printStorePath(drvPath));
-        printError("cannot build derivation '%s': %s dependencies couldn't be built",
-            worker.store.printStorePath(drvPath), nrFailed);
+        logError({
+            .name = "Dependencies could not be built",
+            .hint = hintfmt(
+                "cannot build derivation '%s': %s dependencies couldn't be built",
+                worker.store.printStorePath(drvPath), nrFailed)
+        });
         done(BuildResult::DependencyFailed);
         return;
     }
@@ -1523,7 +1541,7 @@ void DerivationGoal::tryLocalBuild() {
         startBuilder();
 
     } catch (BuildError & e) {
-        printError(e.msg());
+        logError(e.info());
         outputLocks.unlock();
         buildUser.reset();
         worker.permanentFailure = true;
@@ -1740,7 +1758,7 @@ void DerivationGoal::buildDone()
         outputLocks.unlock();
 
     } catch (BuildError & e) {
-        printError(e.msg());
+        logError(e.info());
 
         outputLocks.unlock();
 
@@ -1803,7 +1821,7 @@ HookReply DerivationGoal::tryBuildHook()
             }
         }
 
-        debug(format("hook reply is '%1%'") % reply);
+        debug("hook reply is '%1%'", reply);
 
         if (reply == "decline")
             return rpDecline;
@@ -1819,8 +1837,12 @@ HookReply DerivationGoal::tryBuildHook()
 
     } catch (SysError & e) {
         if (e.errNo == EPIPE) {
-            printError("build hook died unexpectedly: %s",
-                chomp(drainFD(worker.hook->fromHook.readSide.get())));
+            logError({
+                .name = "Build hook died",
+                .hint = hintfmt(
+                    "build hook died unexpectedly: %s",
+                    chomp(drainFD(worker.hook->fromHook.readSide.get())))
+            });
             worker.hook = 0;
             return rpDecline;
         } else
@@ -2000,7 +2022,7 @@ void DerivationGoal::startBuilder()
         string s = get(drv->env, "exportReferencesGraph").value_or("");
         Strings ss = tokenizeString<Strings>(s);
         if (ss.size() % 2 != 0)
-            throw BuildError(format("odd number of tokens in 'exportReferencesGraph': '%1%'") % s);
+            throw BuildError("odd number of tokens in 'exportReferencesGraph': '%1%'", s);
         for (Strings::iterator i = ss.begin(); i != ss.end(); ) {
             string fileName = *i++;
             static std::regex regex("[A-Za-z_][A-Za-z0-9_.-]*");
@@ -2049,7 +2071,7 @@ void DerivationGoal::startBuilder()
                     worker.store.computeFSClosure(worker.store.parseStorePath(worker.store.toStorePath(i.second.source)), closure);
             } catch (InvalidPath & e) {
             } catch (Error & e) {
-                throw Error(format("while processing 'sandbox-paths': %s") % e.what());
+                throw Error("while processing 'sandbox-paths': %s", e.what());
             }
         for (auto & i : closure) {
             auto p = worker.store.printStorePath(i);
@@ -2096,10 +2118,10 @@ void DerivationGoal::startBuilder()
         printMsg(lvlChatty, format("setting up chroot environment in '%1%'") % chrootRootDir);
 
         if (mkdir(chrootRootDir.c_str(), 0750) == -1)
-            throw SysError(format("cannot create '%1%'") % chrootRootDir);
+            throw SysError("cannot create '%1%'", chrootRootDir);
 
         if (buildUser && chown(chrootRootDir.c_str(), 0, buildUser->getGID()) == -1)
-            throw SysError(format("cannot change ownership of '%1%'") % chrootRootDir);
+            throw SysError("cannot change ownership of '%1%'", chrootRootDir);
 
         /* Create a writable /tmp in the chroot.  Many builders need
            this.  (Of course they should really respect $TMPDIR
@@ -2143,7 +2165,7 @@ void DerivationGoal::startBuilder()
         chmod_(chrootStoreDir, 01775);
 
         if (buildUser && chown(chrootStoreDir.c_str(), 0, buildUser->getGID()) == -1)
-            throw SysError(format("cannot change ownership of '%1%'") % chrootStoreDir);
+            throw SysError("cannot change ownership of '%1%'", chrootStoreDir);
 
         for (auto & i : inputPaths) {
             auto p = worker.store.printStorePath(i);
@@ -2176,7 +2198,7 @@ void DerivationGoal::startBuilder()
     if (needsHashRewrite()) {
 
         if (pathExists(homeDir))
-            throw Error(format("home directory '%1%' exists; please remove it to assure purity of builds without sandboxing") % homeDir);
+            throw Error("home directory '%1%' exists; please remove it to assure purity of builds without sandboxing", homeDir);
 
         /* We're not doing a chroot build, but we have some valid
            output paths.  Since we can't just overwrite or delete
@@ -2221,8 +2243,7 @@ void DerivationGoal::startBuilder()
                 if (line == "extra-sandbox-paths" || line == "extra-chroot-dirs") {
                     state = stExtraChrootDirs;
                 } else {
-                    throw Error(format("unknown pre-build hook command '%1%'")
-                        % line);
+                    throw Error("unknown pre-build hook command '%1%'", line);
                 }
             } else if (state == stExtraChrootDirs) {
                 if (line == "") {
@@ -2244,7 +2265,7 @@ void DerivationGoal::startBuilder()
         startDaemon();
 
     /* Run the builder. */
-    printMsg(lvlChatty, format("executing builder '%1%'") % drv->builder);
+    printMsg(lvlChatty, "executing builder '%1%'", drv->builder);
 
     /* Create the log file. */
     Path logFile = openLogFile();
@@ -2982,7 +3003,7 @@ void DerivationGoal::chownToBuilder(const Path & path)
 {
     if (!buildUser) return;
     if (chown(path.c_str(), buildUser->getUID(), buildUser->getGID()) == -1)
-        throw SysError(format("cannot change ownership of '%1%'") % path);
+        throw SysError("cannot change ownership of '%1%'", path);
 }
 
 
@@ -3119,7 +3140,7 @@ void DerivationGoal::runChild()
             /* Bind-mount chroot directory to itself, to treat it as a
                different filesystem from /, as needed for pivot_root. */
             if (mount(chrootRootDir.c_str(), chrootRootDir.c_str(), 0, MS_BIND, 0) == -1)
-                throw SysError(format("unable to bind mount '%1%'") % chrootRootDir);
+                throw SysError("unable to bind mount '%1%'", chrootRootDir);
 
             /* Bind-mount the sandbox's Nix store onto itself so that
                we can mark it as a "shared" subtree, allowing bind
@@ -3181,7 +3202,7 @@ void DerivationGoal::runChild()
                filesystem that we want in the chroot
                environment. */
             auto doBind = [&](const Path & source, const Path & target, bool optional = false) {
-                debug(format("bind mounting '%1%' to '%2%'") % source % target);
+                debug("bind mounting '%1%' to '%2%'", source, target);
                 struct stat st;
                 if (stat(source.c_str(), &st) == -1) {
                     if (optional && errno == ENOENT)
@@ -3253,16 +3274,16 @@ void DerivationGoal::runChild()
 
             /* Do the chroot(). */
             if (chdir(chrootRootDir.c_str()) == -1)
-                throw SysError(format("cannot change directory to '%1%'") % chrootRootDir);
+                throw SysError("cannot change directory to '%1%'", chrootRootDir);
 
             if (mkdir("real-root", 0) == -1)
                 throw SysError("cannot create real-root directory");
 
             if (pivot_root(".", "real-root") == -1)
-                throw SysError(format("cannot pivot old root directory onto '%1%'") % (chrootRootDir + "/real-root"));
+                throw SysError("cannot pivot old root directory onto '%1%'", (chrootRootDir + "/real-root"));
 
             if (chroot(".") == -1)
-                throw SysError(format("cannot change root directory to '%1%'") % chrootRootDir);
+                throw SysError("cannot change root directory to '%1%'", chrootRootDir);
 
             if (umount2("real-root", MNT_DETACH) == -1)
                 throw SysError("cannot unmount real root filesystem");
@@ -3283,7 +3304,7 @@ void DerivationGoal::runChild()
 #endif
 
         if (chdir(tmpDirInSandbox.c_str()) == -1)
-            throw SysError(format("changing into '%1%'") % tmpDir);
+            throw SysError("changing into '%1%'", tmpDir);
 
         /* Close all other file descriptors. */
         closeMostFDs({STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO});
@@ -3422,9 +3443,9 @@ void DerivationGoal::runChild()
                 sandboxProfile += "(allow file-read* file-write* process-exec\n";
                 for (auto & i : dirsInChroot) {
                     if (i.first != i.second.source)
-                        throw Error(format(
-                            "can't map '%1%' to '%2%': mismatched impure paths not supported on Darwin")
-                            % i.first % i.second.source);
+                        throw Error(
+                            "can't map '%1%' to '%2%': mismatched impure paths not supported on Darwin",
+                            i.first, i.second.source);
 
                     string path = i.first;
                     struct stat st;
@@ -3515,7 +3536,7 @@ void DerivationGoal::runChild()
                 else if (drv->builder == "builtin:unpack-channel")
                     builtinUnpackChannel(drv2);
                 else
-                    throw Error(format("unsupported builtin function '%1%'") % string(drv->builder, 8));
+                    throw Error("unsupported builtin function '%1%'", string(drv->builder, 8));
                 _exit(0);
             } catch (std::exception & e) {
                 writeFull(STDERR_FILENO, "error: " + string(e.what()) + "\n");
@@ -3525,7 +3546,7 @@ void DerivationGoal::runChild()
 
         execve(builder, stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data());
 
-        throw SysError(format("executing '%1%'") % drv->builder);
+        throw SysError("executing '%1%'", drv->builder);
 
     } catch (std::exception & e) {
         writeFull(STDERR_FILENO, "\1while setting up the build environment: " + string(e.what()) + "\n");
@@ -3558,7 +3579,7 @@ static void moveCheckToStore(const Path & src, const Path & dst)
        directory's parent link ".."). */
     struct stat st;
     if (lstat(src.c_str(), &st) == -1) {
-        throw SysError(format("getting attributes of path '%1%'") % src);
+        throw SysError("getting attributes of path '%1%'", src);
     }
 
     bool changePerm = (geteuid() && S_ISDIR(st.st_mode) && !(st.st_mode & S_IWUSR));
@@ -3567,7 +3588,7 @@ static void moveCheckToStore(const Path & src, const Path & dst)
         chmod_(src, st.st_mode | S_IWUSR);
 
     if (rename(src.c_str(), dst.c_str()))
-        throw SysError(format("renaming '%1%' to '%2%'") % src % dst);
+        throw SysError("renaming '%1%' to '%2%'", src, dst);
 
     if (changePerm)
         chmod_(dst, st.st_mode);
@@ -3633,7 +3654,7 @@ void DerivationGoal::registerOutputs()
                     replaceValidPath(path, actualPath);
                 else
                     if (buildMode != bmCheck && rename(actualPath.c_str(), worker.store.toRealPath(path).c_str()) == -1)
-                        throw SysError(format("moving build output '%1%' from the sandbox to the Nix store") % path);
+                        throw SysError("moving build output '%1%' from the sandbox to the Nix store", path);
             }
             if (buildMode != bmCheck) actualPath = worker.store.toRealPath(path);
         }
@@ -3654,13 +3675,16 @@ void DerivationGoal::registerOutputs()
            user. */
         if ((!S_ISLNK(st.st_mode) && (st.st_mode & (S_IWGRP | S_IWOTH))) ||
             (buildUser && st.st_uid != buildUser->getUID()))
-            throw BuildError(format("suspicious ownership or permission on '%1%'; rejecting this build output") % path);
+            throw BuildError("suspicious ownership or permission on '%1%'; rejecting this build output", path);
 #endif
 
         /* Apply hash rewriting if necessary. */
         bool rewritten = false;
         if (!outputRewrites.empty()) {
-            printError(format("warning: rewriting hashes in '%1%'; cross fingers") % path);
+            logWarning({
+                .name = "Rewriting hashes",
+                .hint = hintfmt("rewriting hashes in '%1%'; cross fingers", path)
+            });
 
             /* Canonicalise first.  This ensures that the path we're
                rewriting doesn't contain a hard link to /etc/shadow or
@@ -3692,8 +3716,9 @@ void DerivationGoal::registerOutputs()
                 /* The output path should be a regular file without execute permission. */
                 if (!S_ISREG(st.st_mode) || (st.st_mode & S_IXUSR) != 0)
                     throw BuildError(
-                        format("output path '%1%' should be a non-executable regular file "
-                               "since recursive hashing is not enabled (outputHashMode=flat)") % path);
+                        "output path '%1%' should be a non-executable regular file "
+                        "since recursive hashing is not enabled (outputHashMode=flat)",
+                        path);
             }
 
             /* Check the hash. In hash mode, move the path produced by
@@ -3823,10 +3848,10 @@ void DerivationGoal::registerOutputs()
                 result.isNonDeterministic = true;
                 Path prev = worker.store.printStorePath(i->second.path) + checkSuffix;
                 bool prevExists = keepPreviousRound && pathExists(prev);
-                auto msg = prevExists
-                    ? fmt("output '%s' of '%s' differs from '%s' from previous round",
+                hintformat hint = prevExists
+                    ? hintfmt("output '%s' of '%s' differs from '%s' from previous round",
                         worker.store.printStorePath(i->second.path), worker.store.printStorePath(drvPath), prev)
-                    : fmt("output '%s' of '%s' differs from previous round",
+                    : hintfmt("output '%s' of '%s' differs from previous round",
                         worker.store.printStorePath(i->second.path), worker.store.printStorePath(drvPath));
 
                 handleDiffHook(
@@ -3836,9 +3861,14 @@ void DerivationGoal::registerOutputs()
                     worker.store.printStorePath(drvPath), tmpDir);
 
                 if (settings.enforceDeterminism)
-                    throw NotDeterministic(msg);
+                    throw NotDeterministic(hint);
+
+                logError({
+                    .name = "Output determinism error",
+                    .hint = hint
+                });
+
 
-                printError(msg);
                 curRound = nrRounds; // we know enough, bail out early
             }
     }
@@ -4056,7 +4086,7 @@ Path DerivationGoal::openLogFile()
         settings.compressLog ? ".bz2" : "");
 
     fdLogFile = open(logFileName.c_str(), O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, 0666);
-    if (!fdLogFile) throw SysError(format("creating log file '%1%'") % logFileName);
+    if (!fdLogFile) throw SysError("creating log file '%1%'", logFileName);
 
     logFileSink = std::make_shared<FdSink>(fdLogFile.get());
 
@@ -4102,9 +4132,12 @@ void DerivationGoal::handleChildOutput(int fd, const string & data)
     {
         logSize += data.size();
         if (settings.maxLogSize && logSize > settings.maxLogSize) {
-            printError(
-                format("%1% killed after writing more than %2% bytes of log output")
-                % getName() % settings.maxLogSize);
+            logError({
+                .name = "Max log size exceeded",
+                .hint = hintfmt(
+                    "%1% killed after writing more than %2% bytes of log output",
+                    getName(), settings.maxLogSize)
+            });
             killChild();
             done(BuildResult::LogLimitExceeded);
             return;
@@ -4389,7 +4422,7 @@ void SubstitutionGoal::tryNext()
         throw;
     } catch (Error & e) {
         if (settings.tryFallback) {
-            printError(e.what());
+            logError(e.info());
             tryNext();
             return;
         }
@@ -4415,8 +4448,11 @@ void SubstitutionGoal::tryNext()
         && !sub->isTrusted
         && !info->checkSignatures(worker.store, worker.store.getPublicKeys()))
     {
-        printError("warning: substituter '%s' does not have a valid signature for path '%s'",
-            sub->getUri(), worker.store.printStorePath(storePath));
+        logWarning({
+            .name = "Invalid path signature",
+            .hint = hintfmt("substituter '%s' does not have a valid signature for path '%s'",
+                sub->getUri(), worker.store.printStorePath(storePath))
+        });
         tryNext();
         return;
     }
@@ -4559,7 +4595,6 @@ void SubstitutionGoal::handleEOF(int fd)
     if (fd == outPipe.readSide.get()) worker.wakeUp(shared_from_this());
 }
 
-
 //////////////////////////////////////////////////////////////////////
 
 
@@ -4776,9 +4811,9 @@ void Worker::run(const Goals & _topGoals)
         if (!children.empty() || !waitingForAWhile.empty())
             waitForInput();
         else {
-            if (awake.empty() && 0 == settings.maxBuildJobs) throw Error(
-                "unable to start any build; either increase '--max-jobs' "
-                "or enable remote builds");
+            if (awake.empty() && 0 == settings.maxBuildJobs)
+                throw Error("unable to start any build; either increase '--max-jobs' "
+                            "or enable remote builds");
             assert(!awake.empty());
         }
     }
@@ -4791,7 +4826,6 @@ void Worker::run(const Goals & _topGoals)
     assert(!settings.keepGoing || children.empty());
 }
 
-
 void Worker::waitForInput()
 {
     printMsg(lvlVomit, "waiting for children");
@@ -4830,7 +4864,7 @@ void Worker::waitForInput()
     if (!waitingForAWhile.empty()) {
         useTimeout = true;
         if (lastWokenUp == steady_time_point::min())
-            printError("waiting for locks, build slots or build users...");
+            printInfo("waiting for locks, build slots or build users...");
         if (lastWokenUp == steady_time_point::min() || lastWokenUp > before) lastWokenUp = before;
         timeout = std::max(1L,
             (long) std::chrono::duration_cast<std::chrono::seconds>(
@@ -4879,15 +4913,15 @@ void Worker::waitForInput()
                 // FIXME: is there a cleaner way to handle pt close
                 // than EIO? Is this even standard?
                 if (rd == 0 || (rd == -1 && errno == EIO)) {
-                    debug(format("%1%: got EOF") % goal->getName());
+                    debug("%1%: got EOF", goal->getName());
                     goal->handleEOF(k);
                     j->fds.erase(k);
                 } else if (rd == -1) {
                     if (errno != EINTR)
                         throw SysError("%s: read failed", goal->getName());
                 } else {
-                    printMsg(lvlVomit, format("%1%: read %2% bytes")
-                        % goal->getName() % rd);
+                    printMsg(lvlVomit, "%1%: read %2% bytes",
+                        goal->getName(), rd);
                     string data((char *) buffer.data(), rd);
                     j->lastOutput = after;
                     goal->handleChildOutput(k, data);
@@ -4900,9 +4934,12 @@ void Worker::waitForInput()
             j->respectTimeouts &&
             after - j->lastOutput >= std::chrono::seconds(settings.maxSilentTime))
         {
-            printError(
-                format("%1% timed out after %2% seconds of silence")
-                % goal->getName() % settings.maxSilentTime);
+            logError({
+                .name = "Silent build timeout",
+                .hint = hintfmt(
+                    "%1% timed out after %2% seconds of silence",
+                    goal->getName(), settings.maxSilentTime)
+            });
             goal->timedOut();
         }
 
@@ -4911,9 +4948,12 @@ void Worker::waitForInput()
             j->respectTimeouts &&
             after - j->timeStarted >= std::chrono::seconds(settings.buildTimeout))
         {
-            printError(
-                format("%1% timed out after %2% seconds")
-                % goal->getName() % settings.buildTimeout);
+            logError({
+                .name = "Build timeout",
+                .hint = hintfmt(
+                    "%1% timed out after %2% seconds",
+                    goal->getName(), settings.buildTimeout)
+            });
             goal->timedOut();
         }
     }
@@ -4972,7 +5012,11 @@ bool Worker::pathContentsGood(const StorePath & path)
         res = info->narHash == nullHash || info->narHash == current.first;
     }
     pathContentsGoodCache.insert_or_assign(path.clone(), res);
-    if (!res) printError("path '%s' is corrupted or missing!", store.printStorePath(path));
+    if (!res)
+        logError({
+            .name = "Corrupted path",
+            .hint = hintfmt("path '%s' is corrupted or missing!", store.printStorePath(path))
+        });
     return res;
 }
 
@@ -5028,7 +5072,6 @@ void LocalStore::buildPaths(const std::vector<StorePathWithOutputs> & drvPaths,
         throw Error(worker.exitStatus(), "build of %s failed", showPaths(failed));
 }
 
-
 BuildResult LocalStore::buildDerivation(const StorePath & drvPath, const BasicDerivation & drv,
     BuildMode buildMode)
 {
diff --git a/src/libstore/builtins/buildenv.cc b/src/libstore/builtins/buildenv.cc
index 1b802d9081796d58870c92487e3ab84f0391726d..6c493ed770b38208f4d010c778dae1c7d06ea6f0 100644
--- a/src/libstore/builtins/buildenv.cc
+++ b/src/libstore/builtins/buildenv.cc
@@ -22,7 +22,11 @@ static void createLinks(State & state, const Path & srcDir, const Path & dstDir,
         srcFiles = readDirectory(srcDir);
     } catch (SysError & e) {
         if (e.errNo == ENOTDIR) {
-            printError("warning: not including '%s' in the user environment because it's not a directory", srcDir);
+            logWarning(
+                ErrorInfo { 
+                    .name = "Create links - directory",
+                    .hint = hintfmt("not including '%s' in the user environment because it's not a directory", srcDir)
+            });
             return;
         }
         throw;
@@ -41,7 +45,11 @@ static void createLinks(State & state, const Path & srcDir, const Path & dstDir,
                 throw SysError("getting status of '%1%'", srcFile);
         } catch (SysError & e) {
             if (e.errNo == ENOENT || e.errNo == ENOTDIR) {
-                printError("warning: skipping dangling symlink '%s'", dstFile);
+                logWarning(
+                    ErrorInfo { 
+                        .name = "Create links - skipping symlink",
+                        .hint = hintfmt("skipping dangling symlink '%s'", dstFile)
+                });
                 continue;
             }
             throw;
@@ -72,15 +80,15 @@ static void createLinks(State & state, const Path & srcDir, const Path & dstDir,
                     if (!S_ISDIR(lstat(target).st_mode))
                         throw Error("collision between '%1%' and non-directory '%2%'", srcFile, target);
                     if (unlink(dstFile.c_str()) == -1)
-                        throw SysError(format("unlinking '%1%'") % dstFile);
+                        throw SysError("unlinking '%1%'", dstFile);
                     if (mkdir(dstFile.c_str(), 0755) == -1)
-                        throw SysError(format("creating directory '%1%'"));
+                        throw SysError("creating directory '%1%'", dstFile);
                     createLinks(state, target, dstFile, state.priorities[dstFile]);
                     createLinks(state, srcFile, dstFile, priority);
                     continue;
                 }
             } else if (errno != ENOENT)
-                throw SysError(format("getting status of '%1%'") % dstFile);
+                throw SysError("getting status of '%1%'", dstFile);
         }
 
         else {
@@ -99,11 +107,11 @@ static void createLinks(State & state, const Path & srcDir, const Path & dstDir,
                     if (prevPriority < priority)
                         continue;
                     if (unlink(dstFile.c_str()) == -1)
-                        throw SysError(format("unlinking '%1%'") % dstFile);
+                        throw SysError("unlinking '%1%'", dstFile);
                 } else if (S_ISDIR(dstSt.st_mode))
                     throw Error("collision between non-directory '%1%' and directory '%2%'", srcFile, dstFile);
             } else if (errno != ENOENT)
-                throw SysError(format("getting status of '%1%'") % dstFile);
+                throw SysError("getting status of '%1%'", dstFile);
         }
 
         createSymlink(srcFile, dstFile);
diff --git a/src/libstore/builtins/fetchurl.cc b/src/libstore/builtins/fetchurl.cc
index 486babf1427fa4201e8f12b769ac1d77b5e1a0ff..2048f8f876b25c9e7b17bcd2e765e450e0d4ac19 100644
--- a/src/libstore/builtins/fetchurl.cc
+++ b/src/libstore/builtins/fetchurl.cc
@@ -18,7 +18,7 @@ void builtinFetchurl(const BasicDerivation & drv, const std::string & netrcData)
 
     auto getAttr = [&](const string & name) {
         auto i = drv.env.find(name);
-        if (i == drv.env.end()) throw Error(format("attribute '%s' missing") % name);
+        if (i == drv.env.end()) throw Error("attribute '%s' missing", name);
         return i->second;
     };
 
@@ -54,7 +54,7 @@ void builtinFetchurl(const BasicDerivation & drv, const std::string & netrcData)
         auto executable = drv.env.find("executable");
         if (executable != drv.env.end() && executable->second == "1") {
             if (chmod(storePath.c_str(), 0755) == -1)
-                throw SysError(format("making '%1%' executable") % storePath);
+                throw SysError("making '%1%' executable", storePath);
         }
     };
 
diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc
index 877c70a03fcb9dc8042fad252c9d6b308a8799f6..172cfe3cb4813ed0f214c401051fdea4572133e0 100644
--- a/src/libstore/daemon.cc
+++ b/src/libstore/daemon.cc
@@ -73,6 +73,18 @@ struct TunnelLogger : public Logger
         enqueueMsg(*buf.s);
     }
 
+    void logEI(const ErrorInfo & ei) override
+    {
+        if (ei.level > verbosity) return;
+
+        std::stringstream oss; 
+        oss << ei;
+
+        StringSink buf;
+        buf << STDERR_NEXT << oss.str() << "\n"; // (fs.s + "\n");
+        enqueueMsg(*buf.s);
+    }
+
     /* startWork() means that we're starting an operation for which we
       want to send out stderr to the client. */
     void startWork()
@@ -744,7 +756,7 @@ static void performOp(TunnelLogger * logger, ref<Store> store,
     }
 
     default:
-        throw Error(format("invalid operation %1%") % op);
+        throw Error("invalid operation %1%", op);
     }
 }
 
diff --git a/src/libstore/derivations.cc b/src/libstore/derivations.cc
index e268c65ff61ebd09c858d48f5071aa774cd809d9..915e02eed51a9ce46c4eb4c6e38566e80a4273ad 100644
--- a/src/libstore/derivations.cc
+++ b/src/libstore/derivations.cc
@@ -87,7 +87,7 @@ static void expect(std::istream & str, const string & s)
     char s2[s.size()];
     str.read(s2, s.size());
     if (string(s2, s.size()) != s)
-        throw FormatError(format("expected string '%1%'") % s);
+        throw FormatError("expected string '%1%'", s);
 }
 
 
@@ -114,7 +114,7 @@ static Path parsePath(std::istream & str)
 {
     string s = parseString(str);
     if (s.size() == 0 || s[0] != '/')
-        throw FormatError(format("bad path '%1%' in derivation") % s);
+        throw FormatError("bad path '%1%' in derivation", s);
     return s;
 }
 
@@ -196,7 +196,7 @@ Derivation readDerivation(const Store & store, const Path & drvPath)
     try {
         return parseDerivation(store, readFile(drvPath));
     } catch (FormatError & e) {
-        throw Error(format("error parsing derivation '%1%': %2%") % drvPath % e.msg());
+        throw Error("error parsing derivation '%1%': %2%", drvPath, e.msg());
     }
 }
 
diff --git a/src/libstore/filetransfer.cc b/src/libstore/filetransfer.cc
index e9684b3d4950e585eb789d643cfb367286171824..6ca3393abf7fadf563b4f7016edba7231d4a5523 100644
--- a/src/libstore/filetransfer.cc
+++ b/src/libstore/filetransfer.cc
@@ -112,7 +112,7 @@ struct curlFileTransfer : public FileTransfer
             if (requestHeaders) curl_slist_free_all(requestHeaders);
             try {
                 if (!done)
-                    fail(FileTransferError(Interrupted, format("download of '%s' was interrupted") % request.uri));
+                    fail(FileTransferError(Interrupted, "download of '%s' was interrupted", request.uri));
             } catch (...) {
                 ignoreException();
             }
@@ -517,7 +517,7 @@ struct curlFileTransfer : public FileTransfer
             int running;
             CURLMcode mc = curl_multi_perform(curlm, &running);
             if (mc != CURLM_OK)
-                throw nix::Error(format("unexpected error from curl_multi_perform(): %s") % curl_multi_strerror(mc));
+                throw nix::Error("unexpected error from curl_multi_perform(): %s", curl_multi_strerror(mc));
 
             /* Set the promises of any finished requests. */
             CURLMsg * msg;
@@ -547,7 +547,7 @@ struct curlFileTransfer : public FileTransfer
             vomit("download thread waiting for %d ms", sleepTimeMs);
             mc = curl_multi_wait(curlm, extraFDs, 1, sleepTimeMs, &numfds);
             if (mc != CURLM_OK)
-                throw nix::Error(format("unexpected error from curl_multi_wait(): %s") % curl_multi_strerror(mc));
+                throw nix::Error("unexpected error from curl_multi_wait(): %s", curl_multi_strerror(mc));
 
             nextWakeup = std::chrono::steady_clock::time_point();
 
@@ -599,7 +599,11 @@ struct curlFileTransfer : public FileTransfer
             workerThreadMain();
         } catch (nix::Interrupted & e) {
         } catch (std::exception & e) {
-            printError("unexpected error in download thread: %s", e.what());
+            logError({ 
+                .name = "File transfer",
+                .hint = hintfmt("unexpected error in download thread: %s", 
+                                e.what())
+            });
         }
 
         {
diff --git a/src/libstore/filetransfer.hh b/src/libstore/filetransfer.hh
index 2347f363d69b19008bda8b87905be758af2e9d2d..11dca2fe0f0a4ddc6780acf8819cacf08765c5a5 100644
--- a/src/libstore/filetransfer.hh
+++ b/src/libstore/filetransfer.hh
@@ -103,8 +103,9 @@ class FileTransferError : public Error
 {
 public:
     FileTransfer::Error error;
-    FileTransferError(FileTransfer::Error error, const FormatOrString & fs)
-        : Error(fs), error(error)
+    template<typename... Args>
+    FileTransferError(FileTransfer::Error error, const Args & ... args)
+        : Error(args...), error(error)
     { }
 };
 
diff --git a/src/libstore/gc.cc b/src/libstore/gc.cc
index 95a4bc934a84cc343152175d61dc29e756418f44..92f9328c3b8c3291158d0bcda1d3d6eaf303d2c6 100644
--- a/src/libstore/gc.cc
+++ b/src/libstore/gc.cc
@@ -38,10 +38,10 @@ AutoCloseFD LocalStore::openGCLock(LockType lockType)
 
     AutoCloseFD fdGCLock = open(fnGCLock.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600);
     if (!fdGCLock)
-        throw SysError(format("opening global GC lock '%1%'") % fnGCLock);
+        throw SysError("opening global GC lock '%1%'", fnGCLock);
 
     if (!lockFile(fdGCLock.get(), lockType, false)) {
-        printError(format("waiting for the big garbage collector lock..."));
+        printInfo("waiting for the big garbage collector lock...");
         lockFile(fdGCLock.get(), lockType, true);
     }
 
@@ -65,8 +65,8 @@ static void makeSymlink(const Path & link, const Path & target)
 
     /* Atomically replace the old one. */
     if (rename(tempLink.c_str(), link.c_str()) == -1)
-        throw SysError(format("cannot rename '%1%' to '%2%'")
-            % tempLink % link);
+        throw SysError("cannot rename '%1%' to '%2%'",
+            tempLink , link);
 }
 
 
@@ -91,15 +91,15 @@ Path LocalFSStore::addPermRoot(const StorePath & storePath,
     Path gcRoot(canonPath(_gcRoot));
 
     if (isInStore(gcRoot))
-        throw Error(format(
+        throw Error(
                 "creating a garbage collector root (%1%) in the Nix store is forbidden "
-                "(are you running nix-build inside the store?)") % gcRoot);
+                "(are you running nix-build inside the store?)", gcRoot);
 
     if (indirect) {
         /* Don't clobber the link if it already exists and doesn't
            point to the Nix store. */
         if (pathExists(gcRoot) && (!isLink(gcRoot) || !isInStore(readLink(gcRoot))))
-            throw Error(format("cannot create symlink '%1%'; already exists") % gcRoot);
+            throw Error("cannot create symlink '%1%'; already exists", gcRoot);
         makeSymlink(gcRoot, printStorePath(storePath));
         addIndirectRoot(gcRoot);
     }
@@ -109,10 +109,10 @@ Path LocalFSStore::addPermRoot(const StorePath & storePath,
             Path rootsDir = canonPath((format("%1%/%2%") % stateDir % gcRootsDir).str());
 
             if (string(gcRoot, 0, rootsDir.size() + 1) != rootsDir + "/")
-                throw Error(format(
+                throw Error(
                     "path '%1%' is not a valid garbage collector root; "
-                    "it's not in the directory '%2%'")
-                    % gcRoot % rootsDir);
+                    "it's not in the directory '%2%'",
+                    gcRoot, rootsDir);
         }
 
         if (baseNameOf(gcRoot) == std::string(storePath.to_string()))
@@ -129,10 +129,13 @@ Path LocalFSStore::addPermRoot(const StorePath & storePath,
     if (settings.checkRootReachability) {
         auto roots = findRoots(false);
         if (roots[storePath.clone()].count(gcRoot) == 0)
-            printError(
-                "warning: '%1%' is not in a directory where the garbage collector looks for roots; "
+            logWarning(
+                ErrorInfo { 
+                    .name = "GC root",
+                    .hint = hintfmt("warning: '%1%' is not in a directory where the garbage collector looks for roots; "
                 "therefore, '%2%' might be removed by the garbage collector",
-                gcRoot, printStorePath(storePath));
+                gcRoot, printStorePath(storePath))
+            });
     }
 
     /* Grab the global GC root, causing us to block while a GC is in
@@ -170,7 +173,7 @@ void LocalStore::addTempRoot(const StorePath & path)
                way. */
             struct stat st;
             if (fstat(state->fdTempRoots.get(), &st) == -1)
-                throw SysError(format("statting '%1%'") % fnTempRoots);
+                throw SysError("statting '%1%'", fnTempRoots);
             if (st.st_size == 0) break;
 
             /* The garbage collector deleted this file before we could
@@ -216,7 +219,7 @@ void LocalStore::findTempRoots(FDs & fds, Roots & tempRoots, bool censor)
         if (!*fd) {
             /* It's okay if the file has disappeared. */
             if (errno == ENOENT) continue;
-            throw SysError(format("opening temporary roots file '%1%'") % path);
+            throw SysError("opening temporary roots file '%1%'", path);
         }
 
         /* This should work, but doesn't, for some reason. */
@@ -227,7 +230,7 @@ void LocalStore::findTempRoots(FDs & fds, Roots & tempRoots, bool censor)
            only succeed if the owning process has died.  In that case
            we don't care about its temporary roots. */
         if (lockFile(fd->get(), ltWrite, false)) {
-            printError(format("removing stale temporary roots file '%1%'") % path);
+            printInfo("removing stale temporary roots file '%1%'", path);
             unlink(path.c_str());
             writeFull(fd->get(), "d");
             continue;
@@ -403,7 +406,7 @@ void LocalStore::findRuntimeRoots(Roots & roots, bool censor)
                 if (!fdDir) {
                     if (errno == ENOENT || errno == EACCES)
                         continue;
-                    throw SysError(format("opening %1%") % fdStr);
+                    throw SysError("opening %1%", fdStr);
                 }
                 struct dirent * fd_ent;
                 while (errno = 0, fd_ent = readdir(fdDir.get())) {
@@ -413,7 +416,7 @@ void LocalStore::findRuntimeRoots(Roots & roots, bool censor)
                 if (errno) {
                     if (errno == ESRCH)
                         continue;
-                    throw SysError(format("iterating /proc/%1%/fd") % ent->d_name);
+                    throw SysError("iterating /proc/%1%/fd", ent->d_name);
                 }
                 fdDir.reset();
 
@@ -541,7 +544,7 @@ void LocalStore::deletePathRecursive(GCState & state, const Path & path)
     struct stat st;
     if (lstat(realPath.c_str(), &st)) {
         if (errno == ENOENT) return;
-        throw SysError(format("getting status of %1%") % realPath);
+        throw SysError("getting status of %1%", realPath);
     }
 
     printInfo(format("deleting '%1%'") % path);
@@ -559,10 +562,10 @@ void LocalStore::deletePathRecursive(GCState & state, const Path & path)
         // size.
         try {
             if (chmod(realPath.c_str(), st.st_mode | S_IWUSR) == -1)
-                throw SysError(format("making '%1%' writable") % realPath);
+                throw SysError("making '%1%' writable", realPath);
             Path tmp = trashDir + "/" + std::string(baseNameOf(path));
             if (rename(realPath.c_str(), tmp.c_str()))
-                throw SysError(format("unable to rename '%1%' to '%2%'") % realPath % tmp);
+                throw SysError("unable to rename '%1%' to '%2%'", realPath, tmp);
             state.bytesInvalidated += size;
         } catch (SysError & e) {
             if (e.errNo == ENOSPC) {
@@ -681,7 +684,7 @@ void LocalStore::tryToDelete(GCState & state, const Path & path)
 void LocalStore::removeUnusedLinks(const GCState & state)
 {
     AutoCloseDir dir(opendir(linksDir.c_str()));
-    if (!dir) throw SysError(format("opening directory '%1%'") % linksDir);
+    if (!dir) throw SysError("opening directory '%1%'", linksDir);
 
     long long actualSize = 0, unsharedSize = 0;
 
@@ -694,7 +697,7 @@ void LocalStore::removeUnusedLinks(const GCState & state)
 
         struct stat st;
         if (lstat(path.c_str(), &st) == -1)
-            throw SysError(format("statting '%1%'") % path);
+            throw SysError("statting '%1%'", path);
 
         if (st.st_nlink != 1) {
             actualSize += st.st_size;
@@ -705,14 +708,14 @@ void LocalStore::removeUnusedLinks(const GCState & state)
         printMsg(lvlTalkative, format("deleting unused link '%1%'") % path);
 
         if (unlink(path.c_str()) == -1)
-            throw SysError(format("deleting '%1%'") % path);
+            throw SysError("deleting '%1%'", path);
 
         state.results.bytesFreed += st.st_size;
     }
 
     struct stat st;
     if (stat(linksDir.c_str(), &st) == -1)
-        throw SysError(format("statting '%1%'") % linksDir);
+        throw SysError("statting '%1%'", linksDir);
     long long overhead = st.st_blocks * 512ULL;
 
     printInfo(format("note: currently hard linking saves %.2f MiB")
@@ -747,7 +750,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results)
 
     /* Find the roots.  Since we've grabbed the GC lock, the set of
        permanent roots cannot increase now. */
-    printError("finding garbage collector roots...");
+    printInfo("finding garbage collector roots...");
     Roots rootMap;
     if (!options.ignoreLiveness)
         findRootsNoTemp(rootMap, true);
@@ -799,14 +802,14 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results)
     } else if (options.maxFreed > 0) {
 
         if (state.shouldDelete)
-            printError("deleting garbage...");
+            printInfo("deleting garbage...");
         else
-            printError("determining live/dead paths...");
+            printInfo("determining live/dead paths...");
 
         try {
 
             AutoCloseDir dir(opendir(realStoreDir.c_str()));
-            if (!dir) throw SysError(format("opening directory '%1%'") % realStoreDir);
+            if (!dir) throw SysError("opening directory '%1%'", realStoreDir);
 
             /* Read the store and immediately delete all paths that
                aren't valid.  When using --max-freed etc., deleting
@@ -868,7 +871,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results)
 
     /* Clean up the links directory. */
     if (options.action == GCOptions::gcDeleteDead || options.action == GCOptions::gcDeleteSpecific) {
-        printError("deleting unused links...");
+        printInfo("deleting unused links...");
         removeUnusedLinks(state);
     }
 
diff --git a/src/libstore/local-binary-cache-store.cc b/src/libstore/local-binary-cache-store.cc
index 363be14433475233e0ccb1c08ceba358c6e57037..48aca478cc1db60386d8f19bc8ed0e4b9e04529a 100644
--- a/src/libstore/local-binary-cache-store.cc
+++ b/src/libstore/local-binary-cache-store.cc
@@ -74,7 +74,7 @@ static void atomicWrite(const Path & path, const std::string & s)
     AutoDelete del(tmp, false);
     writeFile(tmp, s);
     if (rename(tmp.c_str(), path.c_str()))
-        throw SysError(format("renaming '%1%' to '%2%'") % tmp % path);
+        throw SysError("renaming '%1%' to '%2%'", tmp, path);
     del.cancel();
 }
 
diff --git a/src/libstore/local-fs-store.cc b/src/libstore/local-fs-store.cc
index aa5abd835d71dd25842e6c3dffc04a7364e0ea08..2d564a0d7954042d7e466b3f2fbe7f75f320d11e 100644
--- a/src/libstore/local-fs-store.cc
+++ b/src/libstore/local-fs-store.cc
@@ -22,7 +22,7 @@ struct LocalStoreAccessor : public FSAccessor
     {
         Path storePath = store->toStorePath(path);
         if (!store->isValidPath(store->parseStorePath(storePath)))
-            throw InvalidPath(format("path '%1%' is not a valid store path") % storePath);
+            throw InvalidPath("path '%1%' is not a valid store path", storePath);
         return store->getRealStoreDir() + std::string(path, store->storeDir.size());
     }
 
@@ -33,11 +33,11 @@ struct LocalStoreAccessor : public FSAccessor
         struct stat st;
         if (lstat(realPath.c_str(), &st)) {
             if (errno == ENOENT || errno == ENOTDIR) return {Type::tMissing, 0, false};
-            throw SysError(format("getting status of '%1%'") % path);
+            throw SysError("getting status of '%1%'", path);
         }
 
         if (!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode) && !S_ISLNK(st.st_mode))
-            throw Error(format("file '%1%' has unsupported type") % path);
+            throw Error("file '%1%' has unsupported type", path);
 
         return {
             S_ISREG(st.st_mode) ? Type::tRegular :
diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc
index c2c45b4c277d1d1c763dcadf83651b1dcc3aa9de..5521b8633f66f1072484e153874173f95ba7da80 100644
--- a/src/libstore/local-store.cc
+++ b/src/libstore/local-store.cc
@@ -87,18 +87,22 @@ LocalStore::LocalStore(const Params & params)
 
         struct group * gr = getgrnam(settings.buildUsersGroup.get().c_str());
         if (!gr)
-            printError(format("warning: the group '%1%' specified in 'build-users-group' does not exist")
-                % settings.buildUsersGroup);
+            logError({ 
+                .name = "'build-users-group' not found",
+                .hint = hintfmt(
+                    "warning: the group '%1%' specified in 'build-users-group' does not exist",
+                    settings.buildUsersGroup)
+            });
         else {
             struct stat st;
             if (stat(realStoreDir.c_str(), &st))
-                throw SysError(format("getting attributes of path '%1%'") % realStoreDir);
+                throw SysError("getting attributes of path '%1%'", realStoreDir);
 
             if (st.st_uid != 0 || st.st_gid != gr->gr_gid || (st.st_mode & ~S_IFMT) != perm) {
                 if (chown(realStoreDir.c_str(), 0, gr->gr_gid) == -1)
-                    throw SysError(format("changing ownership of path '%1%'") % realStoreDir);
+                    throw SysError("changing ownership of path '%1%'", realStoreDir);
                 if (chmod(realStoreDir.c_str(), perm) == -1)
-                    throw SysError(format("changing permissions on path '%1%'") % realStoreDir);
+                    throw SysError("changing permissions on path '%1%'", realStoreDir);
             }
         }
     }
@@ -109,12 +113,12 @@ LocalStore::LocalStore(const Params & params)
         struct stat st;
         while (path != "/") {
             if (lstat(path.c_str(), &st))
-                throw SysError(format("getting status of '%1%'") % path);
+                throw SysError("getting status of '%1%'", path);
             if (S_ISLNK(st.st_mode))
-                throw Error(format(
+                throw Error(
                         "the path '%1%' is a symlink; "
-                        "this is not allowed for the Nix store and its parent directories")
-                    % path);
+                        "this is not allowed for the Nix store and its parent directories",
+                        path);
             path = dirOf(path);
         }
     }
@@ -147,7 +151,7 @@ LocalStore::LocalStore(const Params & params)
     globalLock = openLockFile(globalLockPath.c_str(), true);
 
     if (!lockFile(globalLock.get(), ltRead, false)) {
-        printError("waiting for the big Nix store lock...");
+        printInfo("waiting for the big Nix store lock...");
         lockFile(globalLock.get(), ltRead, true);
     }
 
@@ -155,8 +159,8 @@ LocalStore::LocalStore(const Params & params)
        upgrade.  */
     int curSchema = getSchema();
     if (curSchema > nixSchemaVersion)
-        throw Error(format("current Nix store schema is version %1%, but I only support %2%")
-            % curSchema % nixSchemaVersion);
+        throw Error("current Nix store schema is version %1%, but I only support %2%",
+             curSchema, nixSchemaVersion);
 
     else if (curSchema == 0) { /* new store */
         curSchema = nixSchemaVersion;
@@ -178,7 +182,7 @@ LocalStore::LocalStore(const Params & params)
                 "please upgrade Nix to version 1.11 first.");
 
         if (!lockFile(globalLock.get(), ltWrite, false)) {
-            printError("waiting for exclusive access to the Nix store...");
+            printInfo("waiting for exclusive access to the Nix store...");
             lockFile(globalLock.get(), ltWrite, true);
         }
 
@@ -256,7 +260,7 @@ LocalStore::~LocalStore()
     }
 
     if (future.valid()) {
-        printError("waiting for auto-GC to finish on exit...");
+        printInfo("waiting for auto-GC to finish on exit...");
         future.get();
     }
 
@@ -284,7 +288,7 @@ int LocalStore::getSchema()
     if (pathExists(schemaPath)) {
         string s = readFile(schemaPath);
         if (!string2Int(s, curSchema))
-            throw Error(format("'%1%' is corrupt") % schemaPath);
+            throw Error("'%1%' is corrupt", schemaPath);
     }
     return curSchema;
 }
@@ -293,7 +297,7 @@ int LocalStore::getSchema()
 void LocalStore::openDB(State & state, bool create)
 {
     if (access(dbDir.c_str(), R_OK | W_OK))
-        throw SysError(format("Nix database directory '%1%' is not writable") % dbDir);
+        throw SysError("Nix database directory '%1%' is not writable", dbDir);
 
     /* Open the Nix database. */
     string dbPath = dbDir + "/db.sqlite";
@@ -367,7 +371,7 @@ void LocalStore::makeStoreWritable()
             throw SysError("setting up a private mount namespace");
 
         if (mount(0, realStoreDir.c_str(), "none", MS_REMOUNT | MS_BIND, 0) == -1)
-            throw SysError(format("remounting %1% writable") % realStoreDir);
+            throw SysError("remounting %1% writable", realStoreDir);
     }
 #endif
 }
@@ -388,7 +392,7 @@ static void canonicaliseTimestampAndPermissions(const Path & path, const struct
                  | 0444
                  | (st.st_mode & S_IXUSR ? 0111 : 0);
             if (chmod(path.c_str(), mode) == -1)
-                throw SysError(format("changing mode of '%1%' to %2$o") % path % mode);
+                throw SysError("changing mode of '%1%' to %2$o", path, mode);
         }
 
     }
@@ -406,7 +410,7 @@ static void canonicaliseTimestampAndPermissions(const Path & path, const struct
 #else
         if (!S_ISLNK(st.st_mode) && utimes(path.c_str(), times) == -1)
 #endif
-            throw SysError(format("changing modification time of '%1%'") % path);
+            throw SysError("changing modification time of '%1%'", path);
     }
 }
 
@@ -415,7 +419,7 @@ void canonicaliseTimestampAndPermissions(const Path & path)
 {
     struct stat st;
     if (lstat(path.c_str(), &st))
-        throw SysError(format("getting attributes of path '%1%'") % path);
+        throw SysError("getting attributes of path '%1%'", path);
     canonicaliseTimestampAndPermissions(path, st);
 }
 
@@ -430,17 +434,17 @@ static void canonicalisePathMetaData_(const Path & path, uid_t fromUid, InodesSe
        setattrlist() to remove other attributes as well. */
     if (lchflags(path.c_str(), 0)) {
         if (errno != ENOTSUP)
-            throw SysError(format("clearing flags of path '%1%'") % path);
+            throw SysError("clearing flags of path '%1%'", path);
     }
 #endif
 
     struct stat st;
     if (lstat(path.c_str(), &st))
-        throw SysError(format("getting attributes of path '%1%'") % path);
+        throw SysError("getting attributes of path '%1%'", path);
 
     /* Really make sure that the path is of a supported type. */
     if (!(S_ISREG(st.st_mode) || S_ISDIR(st.st_mode) || S_ISLNK(st.st_mode)))
-        throw Error(format("file '%1%' has an unsupported type") % path);
+        throw Error("file '%1%' has an unsupported type", path);
 
 #if __linux__
     /* Remove extended attributes / ACLs. */
@@ -474,7 +478,7 @@ static void canonicalisePathMetaData_(const Path & path, uid_t fromUid, InodesSe
     if (fromUid != (uid_t) -1 && st.st_uid != fromUid) {
         assert(!S_ISDIR(st.st_mode));
         if (inodesSeen.find(Inode(st.st_dev, st.st_ino)) == inodesSeen.end())
-            throw BuildError(format("invalid ownership on file '%1%'") % path);
+            throw BuildError("invalid ownership on file '%1%'", path);
         mode_t mode = st.st_mode & ~S_IFMT;
         assert(S_ISLNK(st.st_mode) || (st.st_uid == geteuid() && (mode == 0444 || mode == 0555) && st.st_mtime == mtimeStore));
         return;
@@ -498,8 +502,8 @@ static void canonicalisePathMetaData_(const Path & path, uid_t fromUid, InodesSe
         if (!S_ISLNK(st.st_mode) &&
             chown(path.c_str(), geteuid(), getegid()) == -1)
 #endif
-            throw SysError(format("changing owner of '%1%' to %2%")
-                % path % geteuid());
+            throw SysError("changing owner of '%1%' to %2%",
+                path, geteuid());
     }
 
     if (S_ISDIR(st.st_mode)) {
@@ -518,11 +522,11 @@ void canonicalisePathMetaData(const Path & path, uid_t fromUid, InodesSeen & ino
        be a symlink, since we can't change its ownership. */
     struct stat st;
     if (lstat(path.c_str(), &st))
-        throw SysError(format("getting attributes of path '%1%'") % path);
+        throw SysError("getting attributes of path '%1%'", path);
 
     if (st.st_uid != geteuid()) {
         assert(S_ISLNK(st.st_mode));
-        throw Error(format("wrong ownership of top-level store path '%1%'") % path);
+        throw Error("wrong ownership of top-level store path '%1%'", path);
     }
 }
 
@@ -859,7 +863,7 @@ void LocalStore::querySubstitutablePathInfos(const StorePathSet & paths,
             } catch (SubstituterDisabled &) {
             } catch (Error & e) {
                 if (settings.tryFallback)
-                    printError(e.what());
+                    logError(e.info());
                 else
                     throw;
             }
@@ -1187,7 +1191,7 @@ void LocalStore::invalidatePathChecked(const StorePath & path)
 
 bool LocalStore::verifyStore(bool checkContents, RepairFlag repair)
 {
-    printError(format("reading the Nix store..."));
+    printInfo(format("reading the Nix store..."));
 
     bool errors = false;
 
@@ -1219,12 +1223,15 @@ bool LocalStore::verifyStore(bool checkContents, RepairFlag repair)
             Path linkPath = linksDir + "/" + link.name;
             string hash = hashPath(htSHA256, linkPath).first.to_string(Base32, false);
             if (hash != link.name) {
-                printError(
-                    "link '%s' was modified! expected hash '%s', got '%s'",
-                    linkPath, link.name, hash);
+                logError({ 
+                    .name = "Invalid hash",
+                    .hint = hintfmt(
+                        "link '%s' was modified! expected hash '%s', got '%s'",
+                        linkPath, link.name, hash)
+                });
                 if (repair) {
                     if (unlink(linkPath.c_str()) == 0)
-                        printError("removed link '%s'", linkPath);
+                        printInfo("removed link '%s'", linkPath);
                     else
                         throw SysError("removing corrupt link '%s'", linkPath);
                 } else {
@@ -1254,8 +1261,11 @@ bool LocalStore::verifyStore(bool checkContents, RepairFlag repair)
                 auto current = hashSink->finish();
 
                 if (info->narHash != nullHash && info->narHash != current.first) {
-                    printError("path '%s' was modified! expected hash '%s', got '%s'",
-                        printStorePath(i), info->narHash.to_string(Base32, true), current.first.to_string(Base32, true));
+                    logError({ 
+                        .name = "Invalid hash - path modified",
+                        .hint = hintfmt("path '%s' was modified! expected hash '%s', got '%s'",
+                        printStorePath(i), info->narHash.to_string(Base32, true), current.first.to_string(Base32, true))
+                    });
                     if (repair) repairPath(i); else errors = true;
                 } else {
 
@@ -1263,14 +1273,14 @@ bool LocalStore::verifyStore(bool checkContents, RepairFlag repair)
 
                     /* Fill in missing hashes. */
                     if (info->narHash == nullHash) {
-                        printError("fixing missing hash on '%s'", printStorePath(i));
+                        printInfo("fixing missing hash on '%s'", printStorePath(i));
                         info->narHash = current.first;
                         update = true;
                     }
 
                     /* Fill in missing narSize fields (from old stores). */
                     if (info->narSize == 0) {
-                        printError("updating size field on '%s' to %s", printStorePath(i), current.second);
+                        printInfo("updating size field on '%s' to %s", printStorePath(i), current.second);
                         info->narSize = current.second;
                         update = true;
                     }
@@ -1286,7 +1296,7 @@ bool LocalStore::verifyStore(bool checkContents, RepairFlag repair)
                 /* It's possible that the path got GC'ed, so ignore
                    errors on invalid paths. */
                 if (isValidPath(i))
-                    printError("error: %s", e.msg());
+                    logError(e.info());
                 else
                     warn(e.msg());
                 errors = true;
@@ -1306,7 +1316,10 @@ void LocalStore::verifyPath(const Path & pathS, const StringSet & store,
     if (!done.insert(pathS).second) return;
 
     if (!isStorePath(pathS)) {
-        printError("path '%s' is not in the Nix store", pathS);
+        logError({ 
+            .name = "Nix path not found",
+            .hint = hintfmt("path '%s' is not in the Nix store", pathS)
+        });
         return;
     }
 
@@ -1325,16 +1338,19 @@ void LocalStore::verifyPath(const Path & pathS, const StringSet & store,
             }
 
         if (canInvalidate) {
-            printError("path '%s' disappeared, removing from database...", pathS);
+            printInfo("path '%s' disappeared, removing from database...", pathS);
             auto state(_state.lock());
             invalidatePath(*state, path);
         } else {
-            printError("path '%s' disappeared, but it still has valid referrers!", pathS);
+            logError({ 
+                .name = "Missing path with referrers",
+                .hint = hintfmt("path '%s' disappeared, but it still has valid referrers!", pathS)
+            });
             if (repair)
                 try {
                     repairPath(path);
                 } catch (Error & e) {
-                    warn(e.msg());
+                    logWarning(e.info());
                     errors = true;
                 }
             else errors = true;
@@ -1374,7 +1390,7 @@ static void makeMutable(const Path & path)
     AutoCloseFD fd = open(path.c_str(), O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
     if (fd == -1) {
         if (errno == ELOOP) return; // it's a symlink
-        throw SysError(format("opening file '%1%'") % path);
+        throw SysError("opening file '%1%'", path);
     }
 
     unsigned int flags = 0, old;
@@ -1392,7 +1408,7 @@ static void makeMutable(const Path & path)
 void LocalStore::upgradeStore7()
 {
     if (getuid() != 0) return;
-    printError("removing immutable bits from the Nix store (this may take a while)...");
+    printInfo("removing immutable bits from the Nix store (this may take a while)...");
     makeMutable(realStoreDir);
 }
 
diff --git a/src/libstore/nar-accessor.cc b/src/libstore/nar-accessor.cc
index b74480684f2a27166d7a52e590e8f5df3abdc638..ca663d83759393b6f5ace1d9bbf2e6440944b99e 100644
--- a/src/libstore/nar-accessor.cc
+++ b/src/libstore/nar-accessor.cc
@@ -184,7 +184,7 @@ struct NarAccessor : public FSAccessor
         auto i = get(path);
 
         if (i.type != FSAccessor::Type::tDirectory)
-            throw Error(format("path '%1%' inside NAR file is not a directory") % path);
+            throw Error("path '%1%' inside NAR file is not a directory", path);
 
         StringSet res;
         for (auto & child : i.children)
@@ -197,7 +197,7 @@ struct NarAccessor : public FSAccessor
     {
         auto i = get(path);
         if (i.type != FSAccessor::Type::tRegular)
-            throw Error(format("path '%1%' inside NAR file is not a regular file") % path);
+            throw Error("path '%1%' inside NAR file is not a regular file", path);
 
         if (getNarBytes) return getNarBytes(i.start, i.size);
 
@@ -209,7 +209,7 @@ struct NarAccessor : public FSAccessor
     {
         auto i = get(path);
         if (i.type != FSAccessor::Type::tSymlink)
-            throw Error(format("path '%1%' inside NAR file is not a symlink") % path);
+            throw Error("path '%1%' inside NAR file is not a symlink", path);
         return i.target;
     }
 };
diff --git a/src/libstore/nar-info.cc b/src/libstore/nar-info.cc
index 6b16be08a1310fc3e683baa47cad7d1c6f727cab..2322847232bb5fe7a511dd78667e3e6db5cb3fc6 100644
--- a/src/libstore/nar-info.cc
+++ b/src/libstore/nar-info.cc
@@ -7,7 +7,7 @@ NarInfo::NarInfo(const Store & store, const std::string & s, const std::string &
     : ValidPathInfo(StorePath::dummy.clone()) // FIXME: hack
 {
     auto corrupt = [&]() {
-        throw Error(format("NAR info file '%1%' is corrupt") % whence);
+        throw Error("NAR info file '%1%' is corrupt", whence);
     };
 
     auto parseHashField = [&](const string & s) {
diff --git a/src/libstore/optimise-store.cc b/src/libstore/optimise-store.cc
index 3f4b72b9c923b080321f4ca7b9bfc999483e1eee..d760d110c7ecd8967aa40db18161384a880ae4a3 100644
--- a/src/libstore/optimise-store.cc
+++ b/src/libstore/optimise-store.cc
@@ -19,9 +19,9 @@ static void makeWritable(const Path & path)
 {
     struct stat st;
     if (lstat(path.c_str(), &st))
-        throw SysError(format("getting attributes of path '%1%'") % path);
+        throw SysError("getting attributes of path '%1%'", path);
     if (chmod(path.c_str(), st.st_mode | S_IWUSR) == -1)
-        throw SysError(format("changing writability of '%1%'") % path);
+        throw SysError("changing writability of '%1%'", path);
 }
 
 
@@ -47,7 +47,7 @@ LocalStore::InodeHash LocalStore::loadInodeHash()
     InodeHash inodeHash;
 
     AutoCloseDir dir(opendir(linksDir.c_str()));
-    if (!dir) throw SysError(format("opening directory '%1%'") % linksDir);
+    if (!dir) throw SysError("opening directory '%1%'", linksDir);
 
     struct dirent * dirent;
     while (errno = 0, dirent = readdir(dir.get())) { /* sic */
@@ -55,7 +55,7 @@ LocalStore::InodeHash LocalStore::loadInodeHash()
         // We don't care if we hit non-hash files, anything goes
         inodeHash.insert(dirent->d_ino);
     }
-    if (errno) throw SysError(format("reading directory '%1%'") % linksDir);
+    if (errno) throw SysError("reading directory '%1%'", linksDir);
 
     printMsg(lvlTalkative, format("loaded %1% hash inodes") % inodeHash.size());
 
@@ -68,7 +68,7 @@ Strings LocalStore::readDirectoryIgnoringInodes(const Path & path, const InodeHa
     Strings names;
 
     AutoCloseDir dir(opendir(path.c_str()));
-    if (!dir) throw SysError(format("opening directory '%1%'") % path);
+    if (!dir) throw SysError("opening directory '%1%'", path);
 
     struct dirent * dirent;
     while (errno = 0, dirent = readdir(dir.get())) { /* sic */
@@ -83,7 +83,7 @@ Strings LocalStore::readDirectoryIgnoringInodes(const Path & path, const InodeHa
         if (name == "." || name == "..") continue;
         names.push_back(name);
     }
-    if (errno) throw SysError(format("reading directory '%1%'") % path);
+    if (errno) throw SysError("reading directory '%1%'", path);
 
     return names;
 }
@@ -96,7 +96,7 @@ void LocalStore::optimisePath_(Activity * act, OptimiseStats & stats,
 
     struct stat st;
     if (lstat(path.c_str(), &st))
-        throw SysError(format("getting attributes of path '%1%'") % path);
+        throw SysError("getting attributes of path '%1%'", path);
 
 #if __APPLE__
     /* HFS/macOS has some undocumented security feature disabling hardlinking for
@@ -130,7 +130,10 @@ void LocalStore::optimisePath_(Activity * act, OptimiseStats & stats,
        NixOS (example: $fontconfig/var/cache being modified).  Skip
        those files.  FIXME: check the modification time. */
     if (S_ISREG(st.st_mode) && (st.st_mode & S_IWUSR)) {
-        printError(format("skipping suspicious writable file '%1%'") % path);
+        logWarning({ 
+            .name = "Suspicious file",
+            .hint = hintfmt("skipping suspicious writable file '%1%'", path)
+        });
         return;
     }
 
@@ -186,7 +189,7 @@ void LocalStore::optimisePath_(Activity * act, OptimiseStats & stats,
        current file with a hard link to that file. */
     struct stat stLink;
     if (lstat(linkPath.c_str(), &stLink))
-        throw SysError(format("getting attributes of path '%1%'") % linkPath);
+        throw SysError("getting attributes of path '%1%'", linkPath);
 
     if (st.st_ino == stLink.st_ino) {
         debug(format("'%1%' is already linked to '%2%'") % path % linkPath);
@@ -194,7 +197,10 @@ void LocalStore::optimisePath_(Activity * act, OptimiseStats & stats,
     }
 
     if (st.st_size != stLink.st_size) {
-        printError(format("removing corrupted link '%1%'") % linkPath);
+        logWarning({ 
+            .name = "Corrupted link",
+            .hint = hintfmt("removing corrupted link '%1%'", linkPath)
+        });
         unlink(linkPath.c_str());
         goto retry;
     }
@@ -229,7 +235,10 @@ void LocalStore::optimisePath_(Activity * act, OptimiseStats & stats,
     /* Atomically replace the old file with the new hard link. */
     if (rename(tempLink.c_str(), path.c_str()) == -1) {
         if (unlink(tempLink.c_str()) == -1)
-            printError(format("unable to unlink '%1%'") % tempLink);
+            logError({ 
+                .name = "Unlink error",
+                .hint = hintfmt("unable to unlink '%1%'", tempLink)
+            });
         if (errno == EMLINK) {
             /* Some filesystems generate too many links on the rename,
                rather than on the original link.  (Probably it
@@ -238,7 +247,7 @@ void LocalStore::optimisePath_(Activity * act, OptimiseStats & stats,
             debug("'%s' has reached maximum number of links", linkPath);
             return;
         }
-        throw SysError(format("cannot rename '%1%' to '%2%'") % tempLink % path);
+        throw SysError("cannot rename '%1%' to '%2%'", tempLink, path);
     }
 
     stats.filesLinked++;
diff --git a/src/libstore/pathlocks.cc b/src/libstore/pathlocks.cc
index 2635e3940af891560396f2c76ae183edd6bfd7d5..926f4ea1e44fa1cb09bd3a8e76b965ca8391c0ae 100644
--- a/src/libstore/pathlocks.cc
+++ b/src/libstore/pathlocks.cc
@@ -20,7 +20,7 @@ AutoCloseFD openLockFile(const Path & path, bool create)
 
     fd = open(path.c_str(), O_CLOEXEC | O_RDWR | (create ? O_CREAT : 0), 0600);
     if (!fd && (create || errno != ENOENT))
-        throw SysError(format("opening lock file '%1%'") % path);
+        throw SysError("opening lock file '%1%'", path);
 
     return fd;
 }
@@ -51,7 +51,7 @@ bool lockFile(int fd, LockType lockType, bool wait)
         while (flock(fd, type) != 0) {
             checkInterrupt();
             if (errno != EINTR)
-                throw SysError(format("acquiring/releasing lock"));
+                throw SysError("acquiring/releasing lock");
             else
                 return false;
         }
@@ -60,7 +60,7 @@ bool lockFile(int fd, LockType lockType, bool wait)
             checkInterrupt();
             if (errno == EWOULDBLOCK) return false;
             if (errno != EINTR)
-                throw SysError(format("acquiring/releasing lock"));
+                throw SysError("acquiring/releasing lock");
         }
     }
 
@@ -124,7 +124,7 @@ bool PathLocks::lockPaths(const PathSet & paths,
                hasn't been unlinked). */
             struct stat st;
             if (fstat(fd.get(), &st) == -1)
-                throw SysError(format("statting lock file '%1%'") % lockPath);
+                throw SysError("statting lock file '%1%'", lockPath);
             if (st.st_size != 0)
                 /* This lock file has been unlinked, so we're holding
                    a lock on a deleted file.  This means that other
@@ -160,7 +160,8 @@ void PathLocks::unlock()
 
         if (close(i.first) == -1)
             printError(
-                format("error (ignored): cannot close lock file on '%1%'") % i.second);
+                "error (ignored): cannot close lock file on '%1%'",
+                i.second);
 
         debug(format("lock released on '%1%'") % i.second);
     }
diff --git a/src/libstore/profiles.cc b/src/libstore/profiles.cc
index 2bef518786c6a078e8741531383cb9e974ac841f..6cfe393a45e39b85faf8d913f512202285942a16 100644
--- a/src/libstore/profiles.cc
+++ b/src/libstore/profiles.cc
@@ -50,7 +50,7 @@ Generations findGenerations(Path profile, int & curGen)
             gen.number = n;
             struct stat st;
             if (lstat(gen.path.c_str(), &st) != 0)
-                throw SysError(format("statting '%1%'") % gen.path);
+                throw SysError("statting '%1%'", gen.path);
             gen.creationTime = st.st_mtime;
             gens.push_back(gen);
         }
@@ -117,7 +117,7 @@ Path createGeneration(ref<LocalFSStore> store, Path profile, Path outPath)
 static void removeFile(const Path & path)
 {
     if (remove(path.c_str()) == -1)
-        throw SysError(format("cannot unlink '%1%'") % path);
+        throw SysError("cannot unlink '%1%'", path);
 }
 
 
@@ -149,7 +149,7 @@ void deleteGenerations(const Path & profile, const std::set<unsigned int> & gens
     Generations gens = findGenerations(profile, curGen);
 
     if (gensToDelete.find(curGen) != gensToDelete.end())
-        throw Error(format("cannot delete current generation of profile %1%'") % profile);
+        throw Error("cannot delete current generation of profile %1%'", profile);
 
     for (auto & i : gens) {
         if (gensToDelete.find(i.number) == gensToDelete.end()) continue;
@@ -226,7 +226,7 @@ void deleteGenerationsOlderThan(const Path & profile, const string & timeSpec, b
     int days;
 
     if (!string2Int(strDays, days) || days < 1)
-        throw Error(format("invalid number of days specifier '%1%'") % timeSpec);
+        throw Error("invalid number of days specifier '%1%'", timeSpec);
 
     time_t oldTime = curTime - days * 24 * 3600;
 
diff --git a/src/libstore/references.cc b/src/libstore/references.cc
index 102e1592180a8e51db975bdfbb13f1652399aeab..a10d536a35565586911ea144dce7d7a25e1b912e 100644
--- a/src/libstore/references.cc
+++ b/src/libstore/references.cc
@@ -92,7 +92,7 @@ PathSet scanForReferences(const string & path,
         auto baseName = std::string(baseNameOf(i));
         string::size_type pos = baseName.find('-');
         if (pos == string::npos)
-            throw Error(format("bad reference '%1%'") % i);
+            throw Error("bad reference '%1%'", i);
         string s = string(baseName, 0, pos);
         assert(s.size() == refLength);
         assert(backMap.find(s) == backMap.end());
diff --git a/src/libstore/remote-fs-accessor.cc b/src/libstore/remote-fs-accessor.cc
index 5a2d103b97338106d2c276c2404ad76cde9abeff..9277a8e6bc91c0ada289c590392506bd8981182c 100644
--- a/src/libstore/remote-fs-accessor.cc
+++ b/src/libstore/remote-fs-accessor.cc
@@ -51,7 +51,7 @@ std::pair<ref<FSAccessor>, Path> RemoteFSAccessor::fetch(const Path & path_)
     std::string restPath = std::string(path, storePath.size());
 
     if (!store->isValidPath(store->parseStorePath(storePath)))
-        throw InvalidPath(format("path '%1%' is not a valid store path") % storePath);
+        throw InvalidPath("path '%1%' is not a valid store path", storePath);
 
     auto i = nars.find(storePath);
     if (i != nars.end()) return {i->second, restPath};
diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc
index 735f59a91ade97bb3676bc8aa7a5b9900aba2e3a..99fee81506b453bab8cd219ecbf423ba43de6823 100644
--- a/src/libstore/remote-store.cc
+++ b/src/libstore/remote-store.cc
@@ -116,11 +116,11 @@ ref<RemoteStore::Connection> UDSRemoteStore::openConnection()
     struct sockaddr_un addr;
     addr.sun_family = AF_UNIX;
     if (socketPath.size() + 1 >= sizeof(addr.sun_path))
-        throw Error(format("socket path '%1%' is too long") % socketPath);
+        throw Error("socket path '%1%' is too long", socketPath);
     strcpy(addr.sun_path, socketPath.c_str());
 
     if (::connect(conn->fd.get(), (struct sockaddr *) &addr, sizeof(addr)) == -1)
-        throw SysError(format("cannot connect to daemon at '%1%'") % socketPath);
+        throw SysError("cannot connect to daemon at '%1%'", socketPath);
 
     conn->from.fd = conn->fd.get();
     conn->to.fd = conn->fd.get();
@@ -365,7 +365,7 @@ void RemoteStore::queryPathInfoUncached(const StorePath & path,
             } catch (Error & e) {
                 // Ugly backwards compatibility hack.
                 if (e.msg().find("is not valid") != std::string::npos)
-                    throw InvalidPath(e.what());
+                    throw InvalidPath(e.info());
                 throw;
             }
             if (GET_PROTOCOL_MINOR(conn->daemonVersion) >= 17) {
diff --git a/src/libstore/s3-binary-cache-store.cc b/src/libstore/s3-binary-cache-store.cc
index b24e7b7d621a19d242203abbfeb732a23e283c6d..427dd48ce36669260e1faefeb7a0176c01e7b6b9 100644
--- a/src/libstore/s3-binary-cache-store.cc
+++ b/src/libstore/s3-binary-cache-store.cc
@@ -32,8 +32,10 @@ namespace nix {
 struct S3Error : public Error
 {
     Aws::S3::S3Errors err;
-    S3Error(Aws::S3::S3Errors err, const FormatOrString & fs)
-        : Error(fs), err(err) { };
+
+    template<typename... Args>
+    S3Error(Aws::S3::S3Errors err, const Args & ... args)
+        : Error(args...), err(err) { };
 };
 
 /* Helper: given an Outcome<R, E>, return R in case of success, or
@@ -109,7 +111,9 @@ class RetryStrategy : public Aws::Client::DefaultRetryStrategy
         auto retry = Aws::Client::DefaultRetryStrategy::ShouldRetry(error, attemptedRetries);
         if (retry)
             printError("AWS error '%s' (%s), will retry in %d ms",
-                error.GetExceptionName(), error.GetMessage(), CalculateDelayBeforeNextRetry(error, attemptedRetries));
+                error.GetExceptionName(),
+                error.GetMessage(),
+                CalculateDelayBeforeNextRetry(error, attemptedRetries));
         return retry;
     }
 };
@@ -249,7 +253,7 @@ struct S3BinaryCacheStoreImpl : public S3BinaryCacheStore
                 // If bucket listing is disabled, 404s turn into 403s
                 || error.GetErrorType() == Aws::S3::S3Errors::ACCESS_DENIED)
                 return false;
-            throw Error(format("AWS error fetching '%s': %s") % path % error.GetMessage());
+            throw Error("AWS error fetching '%s': %s", path, error.GetMessage());
         }
 
         return true;
diff --git a/src/libstore/sqlite.cc b/src/libstore/sqlite.cc
index eb1daafc5b89f14e3a88b6bf02877ea07db16c78..16cdb6619b4d71daf344260b946888c92fd4f526 100644
--- a/src/libstore/sqlite.cc
+++ b/src/libstore/sqlite.cc
@@ -29,7 +29,7 @@ SQLite::SQLite(const Path & path, bool create)
 {
     if (sqlite3_open_v2(path.c_str(), &db,
             SQLITE_OPEN_READWRITE | (create ? SQLITE_OPEN_CREATE : 0), 0) != SQLITE_OK)
-        throw Error(format("cannot open SQLite database '%s'") % path);
+        throw Error("cannot open SQLite database '%s'", path);
 
     if (sqlite3_busy_timeout(db, 60 * 60 * 1000) != SQLITE_OK)
         throwSQLiteError(db, "setting timeout");
@@ -204,7 +204,10 @@ void handleSQLiteBusy(const SQLiteBusy & e)
 
     if (now > lastWarned + 10) {
         lastWarned = now;
-        printError("warning: %s", e.what());
+        logWarning(
+            ErrorInfo { .name = "Sqlite busy",
+                        .hint = hintfmt(e.what())
+            });
     }
 
     /* Sleep for a while since retrying the transaction right away
diff --git a/src/libstore/sqlite.hh b/src/libstore/sqlite.hh
index fd04c9b079a0138553c04d354843dca3fd3f592f..dd81ab0513547b8995359d67cf45efc8192b7d50 100644
--- a/src/libstore/sqlite.hh
+++ b/src/libstore/sqlite.hh
@@ -3,7 +3,7 @@
 #include <functional>
 #include <string>
 
-#include "types.hh"
+#include "error.hh"
 
 struct sqlite3;
 struct sqlite3_stmt;
diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc
index f6901bf422cb0dafee57da6b493ec8e600c1d103..4642610105d84c74e0ec7bd953830d7ebcba961f 100644
--- a/src/libstore/store-api.cc
+++ b/src/libstore/store-api.cc
@@ -23,7 +23,7 @@ bool Store::isInStore(const Path & path) const
 Path Store::toStorePath(const Path & path) const
 {
     if (!isInStore(path))
-        throw Error(format("path '%1%' is not in the Nix store") % path);
+        throw Error("path '%1%' is not in the Nix store", path);
     Path::size_type slash = path.find('/', storeDir.size() + 1);
     if (slash == Path::npos)
         return path;
@@ -775,7 +775,11 @@ void ValidPathInfo::sign(const Store & store, const SecretKey & secretKey)
 bool ValidPathInfo::isContentAddressed(const Store & store) const
 {
     auto warn = [&]() {
-        printError("warning: path '%s' claims to be content-addressed but isn't", store.printStorePath(path));
+        logWarning(
+            ErrorInfo{
+                .name = "Path not content-addressed", 
+                .hint = hintfmt("path '%s' claims to be content-addressed but isn't", store.printStorePath(path))
+            });
     };
 
     if (hasPrefix(ca, "text:")) {
@@ -934,7 +938,7 @@ std::list<ref<Store>> getDefaultSubstituters()
             try {
                 stores.push_back(openStore(uri));
             } catch (Error & e) {
-                printError("warning: %s", e.what());
+                logWarning(e.info());
             }
         };
 
diff --git a/src/libutil/affinity.cc b/src/libutil/affinity.cc
index 98f8287ada67bfdb80e2cb05b7b8c482330bf3cb..ac2295e4ab7adb61abd94d3048efc46b072b2063 100644
--- a/src/libutil/affinity.cc
+++ b/src/libutil/affinity.cc
@@ -12,6 +12,17 @@ namespace nix {
 #if __linux__
 static bool didSaveAffinity = false;
 static cpu_set_t savedAffinity;
+
+std::ostream& operator<<(std::ostream &os, const cpu_set_t &cset)
+{
+    auto count = CPU_COUNT(&cset);
+    for (int i=0; i < count; ++i)
+    {
+        os << (CPU_ISSET(i,&cset) ? "1" : "0");
+    }
+
+    return os;
+}
 #endif
 
 
@@ -25,7 +36,7 @@ void setAffinityTo(int cpu)
     CPU_ZERO(&newAffinity);
     CPU_SET(cpu, &newAffinity);
     if (sched_setaffinity(0, sizeof(cpu_set_t), &newAffinity) == -1)
-        printError(format("failed to lock thread to CPU %1%") % cpu);
+        printError("failed to lock thread to CPU %1%", cpu);
 #endif
 }
 
@@ -47,7 +58,11 @@ void restoreAffinity()
 #if __linux__
     if (!didSaveAffinity) return;
     if (sched_setaffinity(0, sizeof(cpu_set_t), &savedAffinity) == -1)
-        printError("failed to restore affinity %1%");
+    {
+        std::ostringstream oss;
+        oss << savedAffinity;
+        printError("failed to restore CPU affinity %1%", oss.str());
+    }
 #endif
 }
 
diff --git a/src/libutil/archive.cc b/src/libutil/archive.cc
index db544a212ab78955e755288e83178bf7103e9520..6a848470520a9aa656d9eb12e28d97f1a37e1b30 100644
--- a/src/libutil/archive.cc
+++ b/src/libutil/archive.cc
@@ -46,7 +46,7 @@ static void dumpContents(const Path & path, size_t size,
     sink << "contents" << size;
 
     AutoCloseFD fd = open(path.c_str(), O_RDONLY | O_CLOEXEC);
-    if (!fd) throw SysError(format("opening file '%1%'") % path);
+    if (!fd) throw SysError("opening file '%1%'", path);
 
     std::vector<unsigned char> buf(65536);
     size_t left = size;
@@ -68,7 +68,7 @@ static void dump(const Path & path, Sink & sink, PathFilter & filter)
 
     struct stat st;
     if (lstat(path.c_str(), &st))
-        throw SysError(format("getting attributes of path '%1%'") % path);
+        throw SysError("getting attributes of path '%1%'", path);
 
     sink << "(";
 
@@ -94,8 +94,9 @@ static void dump(const Path & path, Sink & sink, PathFilter & filter)
                     name.erase(pos);
                 }
                 if (unhacked.find(name) != unhacked.end())
-                    throw Error(format("file name collision in between '%1%' and '%2%'")
-                        % (path + "/" + unhacked[name]) % (path + "/" + i.name));
+                    throw Error("file name collision in between '%1%' and '%2%'",
+                       (path + "/" + unhacked[name]),
+                       (path + "/" + i.name));
                 unhacked[name] = i.name;
             } else
                 unhacked[i.name] = i.name;
@@ -111,7 +112,7 @@ static void dump(const Path & path, Sink & sink, PathFilter & filter)
     else if (S_ISLNK(st.st_mode))
         sink << "type" << "symlink" << "target" << readLink(path);
 
-    else throw Error(format("file '%1%' has an unsupported type") % path);
+    else throw Error("file '%1%' has an unsupported type", path);
 
     sink << ")";
 }
@@ -247,7 +248,7 @@ static void parse(ParseSink & sink, Source & source, const Path & path)
                 } else if (s == "name") {
                     name = readString(source);
                     if (name.empty() || name == "." || name == ".." || name.find('/') != string::npos || name.find((char) 0) != string::npos)
-                        throw Error(format("NAR contains invalid file name '%1%'") % name);
+                        throw Error("NAR contains invalid file name '%1%'", name);
                     if (name <= prevName)
                         throw Error("NAR directory is not sorted");
                     prevName = name;
@@ -303,14 +304,14 @@ struct RestoreSink : ParseSink
     {
         Path p = dstPath + path;
         if (mkdir(p.c_str(), 0777) == -1)
-            throw SysError(format("creating directory '%1%'") % p);
+            throw SysError("creating directory '%1%'", p);
     };
 
     void createRegularFile(const Path & path)
     {
         Path p = dstPath + path;
         fd = open(p.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0666);
-        if (!fd) throw SysError(format("creating file '%1%'") % p);
+        if (!fd) throw SysError("creating file '%1%'", p);
     }
 
     void isExecutable()
@@ -332,7 +333,7 @@ struct RestoreSink : ParseSink
                OpenSolaris).  Since preallocation is just an
                optimisation, ignore it. */
             if (errno && errno != EINVAL && errno != EOPNOTSUPP && errno != ENOSYS)
-                throw SysError(format("preallocating file of %1% bytes") % len);
+                throw SysError("preallocating file of %1% bytes", len);
         }
 #endif
     }
diff --git a/src/libutil/args.cc b/src/libutil/args.cc
index afeaf4cea5d0be29188073a3b9474225b516ea23..ce65801191aeb6aca7959f52ec8f627bab0c2f6b 100644
--- a/src/libutil/args.cc
+++ b/src/libutil/args.cc
@@ -45,7 +45,7 @@ void Args::parseCmdline(const Strings & _cmdline)
         }
         else if (!dashDash && std::string(arg, 0, 1) == "-") {
             if (!processFlag(pos, cmdline.end()))
-                throw UsageError(format("unrecognised flag '%1%'") % arg);
+                throw UsageError("unrecognised flag '%1%'", arg);
         }
         else {
             pendingArgs.push_back(*pos++);
@@ -130,7 +130,7 @@ bool Args::processArgs(const Strings & args, bool finish)
 {
     if (expectedArgs.empty()) {
         if (!args.empty())
-            throw UsageError(format("unexpected argument '%1%'") % args.front());
+            throw UsageError("unexpected argument '%1%'", args.front());
         return true;
     }
 
diff --git a/src/libutil/compression.cc b/src/libutil/compression.cc
index 860b04adb79496cd2bebc10875c02e975322146d..a117ddc72e1ae3e51881606737ff81090321ef3f 100644
--- a/src/libutil/compression.cc
+++ b/src/libutil/compression.cc
@@ -481,7 +481,7 @@ ref<CompressionSink> makeCompressionSink(const std::string & method, Sink & next
     else if (method == "br")
         return make_ref<BrotliCompressionSink>(nextSink);
     else
-        throw UnknownCompressionMethod(format("unknown compression method '%s'") % method);
+        throw UnknownCompressionMethod("unknown compression method '%s'", method);
 }
 
 ref<std::string> compress(const std::string & method, const std::string & in, const bool parallel)
diff --git a/src/libutil/error.cc b/src/libutil/error.cc
index a5571d4ecb6c5b195c1a5d1e18f524fa7ed381cf..1fcb8111c5ecc3fad5963c1342a4905b56df86e8 100644
--- a/src/libutil/error.cc
+++ b/src/libutil/error.cc
@@ -2,9 +2,38 @@
 
 #include <iostream>
 #include <optional>
+#include "serialise.hh"
+#include <sstream>
 
-namespace nix
+namespace nix {
+
+
+const std::string nativeSystem = SYSTEM;
+
+// addPrefix is used for show-trace.  Strings added with addPrefix
+// will print ahead of the error itself.
+BaseError & BaseError::addPrefix(const FormatOrString & fs)
+{
+    prefix_ = fs.s + prefix_;
+    return *this;
+}
+
+// c++ std::exception descendants must have a 'const char* what()' function.
+// This stringifies the error and caches it for use by what(), or similarly by msg().
+const string& BaseError::calcWhat() const
 {
+    if (what_.has_value())
+        return *what_;
+    else {
+        err.name = sname();
+
+        std::ostringstream oss;
+        oss << err;
+        what_ = oss.str();
+
+        return *what_;
+    }
+}
 
 std::optional<string> ErrorInfo::programName = std::nullopt;
 
@@ -15,80 +44,118 @@ std::ostream& operator<<(std::ostream &os, const hintformat &hf)
 
 string showErrPos(const ErrPos &errPos)
 {
-    if (errPos.column > 0) {
-        return fmt("(%1%:%2%)", errPos.lineNumber, errPos.column);
-    } else {
-        return fmt("(%1%)", errPos.lineNumber);
-    };
+    if (errPos.line > 0) {
+        if (errPos.column > 0) {
+            return fmt("(%1%:%2%)", errPos.line, errPos.column);
+        } else {
+            return fmt("(%1%)", errPos.line);
+        }
+    }
+    else {
+        return "";
+    }
 }
 
-void printCodeLines(const string &prefix, const NixCode &nixCode)
+// if nixCode contains lines of code, print them to the ostream, indicating the error column.
+void printCodeLines(std::ostream &out, const string &prefix, const NixCode &nixCode)
 {
     // previous line of code.
     if (nixCode.prevLineOfCode.has_value()) {
-        std::cout << fmt("%1% %|2$5d|| %3%",
-                         prefix,
-                         (nixCode.errPos.lineNumber - 1),
-                         *nixCode.prevLineOfCode)
-                  << std::endl;
+        out << std::endl 
+            << fmt("%1% %|2$5d|| %3%",
+                prefix,
+                (nixCode.errPos.line - 1),
+                *nixCode.prevLineOfCode);
     }
 
-    // line of code containing the error.%2$+5d%
-    std::cout << fmt("%1% %|2$5d|| %3%",
-                     prefix,
-                     (nixCode.errPos.lineNumber),
-                     nixCode.errLineOfCode)
-              << std::endl;
-
-    // error arrows for the column range.
-    if (nixCode.errPos.column > 0) {
-        int start = nixCode.errPos.column;
-        std::string spaces;
-        for (int i = 0; i < start; ++i) {
-            spaces.append(" ");
+    if (nixCode.errLineOfCode.has_value()) {
+        // line of code containing the error.
+        out << std::endl
+            << fmt("%1% %|2$5d|| %3%",
+                prefix,
+                (nixCode.errPos.line),
+                *nixCode.errLineOfCode);
+        // error arrows for the column range.
+        if (nixCode.errPos.column > 0) {
+            int start = nixCode.errPos.column;
+            std::string spaces;
+            for (int i = 0; i < start; ++i) {
+                spaces.append(" ");
+            }
+
+            std::string arrows("^");
+
+            out << std::endl
+                << fmt("%1%      |%2%" ANSI_RED "%3%" ANSI_NORMAL,
+                    prefix,
+                    spaces,
+                    arrows);
         }
-
-        std::string arrows("^");
-
-        std::cout << fmt("%1%      |%2%" ANSI_RED "%3%" ANSI_NORMAL,
-                         prefix,
-                         spaces,
-                         arrows) << std::endl;
     }
 
     // next line of code.
     if (nixCode.nextLineOfCode.has_value()) {
-        std::cout << fmt("%1% %|2$5d|| %3%",
-                         prefix,
-                         (nixCode.errPos.lineNumber + 1),
-                         *nixCode.nextLineOfCode)
-                  << std::endl;
+        out << std::endl
+            << fmt("%1% %|2$5d|| %3%",
+                prefix,
+                (nixCode.errPos.line + 1),
+                *nixCode.nextLineOfCode);
     }
 }
 
-void printErrorInfo(const ErrorInfo &einfo)
+std::ostream& operator<<(std::ostream &out, const ErrorInfo &einfo)
 {
     int errwidth = 80;
-    string prefix = "    ";
+    string prefix = "";
 
     string levelString;
     switch (einfo.level) {
-    case ErrLevel::elError: {
-        levelString = ANSI_RED;
-        levelString += "error:";
-        levelString += ANSI_NORMAL;
-        break;
-    }
-    case ErrLevel::elWarning: {
-        levelString = ANSI_YELLOW;
-        levelString += "warning:";
-        levelString += ANSI_NORMAL;
-        break;
-    }
-    default: {
-        levelString = fmt("invalid error level: %1%", einfo.level);
-        break;
-    }
+        case Verbosity::lvlError: {
+            levelString = ANSI_RED;
+            levelString += "error:";
+            levelString += ANSI_NORMAL;
+            break;
+        }
+        case Verbosity::lvlWarn: {
+            levelString = ANSI_YELLOW;
+            levelString += "warning:";
+            levelString += ANSI_NORMAL;
+            break;
+        }
+        case Verbosity::lvlInfo: {
+            levelString = ANSI_GREEN;
+            levelString += "info:";
+            levelString += ANSI_NORMAL;
+            break;
+        }
+        case Verbosity::lvlTalkative: {
+            levelString = ANSI_GREEN;
+            levelString += "talk:";
+            levelString += ANSI_NORMAL;
+            break;
+        }
+        case Verbosity::lvlChatty: {
+            levelString = ANSI_GREEN;
+            levelString += "chat:";
+            levelString += ANSI_NORMAL;
+            break;
+        }
+        case Verbosity::lvlVomit: {
+            levelString = ANSI_GREEN;
+            levelString += "vomit:";
+            levelString += ANSI_NORMAL;
+            break;
+        }
+        case Verbosity::lvlDebug: {
+            levelString = ANSI_YELLOW;
+            levelString += "debug:";
+            levelString += ANSI_NORMAL;
+            break;
+        }
+        default: {
+            levelString = fmt("invalid error level: %1%", einfo.level);
+            break;
+        }
     }
 
     int ndl = prefix.length() + levelString.length() + 3 + einfo.name.length() + einfo.programName.value_or("").length();
@@ -99,48 +166,58 @@ void printErrorInfo(const ErrorInfo &einfo)
         dashes.append("-");
 
     // divider.
-    std::cout << fmt("%1%%2%" ANSI_BLUE " %3% %4% %5% %6%" ANSI_NORMAL,
-                     prefix,
-                     levelString,
-                     "---",
-                     einfo.name,
-                     dashes,
-                     einfo.programName.value_or(""))
-              << std::endl;
-
-    // filename.
+    if (einfo.name != "")
+        out << fmt("%1%%2%" ANSI_BLUE " --- %3% %4% %5%" ANSI_NORMAL,
+            prefix,
+            levelString,
+            einfo.name,
+            dashes,
+            einfo.programName.value_or(""));
+    else
+        out << fmt("%1%%2%" ANSI_BLUE " -----%3% %4%" ANSI_NORMAL,
+            prefix,
+            levelString,
+            dashes,
+            einfo.programName.value_or(""));
+
+    bool nl = false;  // intersperse newline between sections.
     if (einfo.nixCode.has_value()) {
-        if (einfo.nixCode->errPos.nixFile != "") {
-            string eline = einfo.nixCode->errLineOfCode != ""
-                           ? string(" ") + showErrPos(einfo.nixCode->errPos)
-                           : "";
-
-            std::cout << fmt("%1%in file: " ANSI_BLUE "%2%%3%" ANSI_NORMAL,
-                             prefix, 
-                             einfo.nixCode->errPos.nixFile,
-                             eline) << std::endl;
-            std::cout << prefix << std::endl;
+        if (einfo.nixCode->errPos.file != "") {
+            // filename, line, column.
+            out << std::endl << fmt("%1%in file: " ANSI_BLUE "%2% %3%" ANSI_NORMAL,
+                prefix,
+                einfo.nixCode->errPos.file,
+                showErrPos(einfo.nixCode->errPos));
         } else {
-            std::cout << fmt("%1%from command line argument", prefix) << std::endl;
-            std::cout << prefix << std::endl;
+            out << std::endl << fmt("%1%from command line argument", prefix);
         }
+        nl = true;
     }
 
     // description
-    std::cout << prefix << einfo.description << std::endl;
-    std::cout << prefix << std::endl;
+    if (einfo.description != "") {
+        if (nl)
+            out << std::endl << prefix;
+        out << std::endl << prefix << einfo.description;
+        nl = true;
+    }
 
     // lines of code.
-    if (einfo.nixCode->errLineOfCode != "") {
-        printCodeLines(prefix, *einfo.nixCode);
-        std::cout << prefix << std::endl;
+    if (einfo.nixCode.has_value() && einfo.nixCode->errLineOfCode.has_value()) {
+        if (nl)
+            out << std::endl << prefix;
+        printCodeLines(out, prefix, *einfo.nixCode);
+        nl = true;
     }
 
     // hint
     if (einfo.hint.has_value()) {
-        std::cout << prefix << *einfo.hint << std::endl;
-        std::cout << prefix << std::endl;
+        if (nl)
+            out << std::endl << prefix;
+        out << std::endl << prefix << *einfo.hint;
+        nl = true;
     }
-}
 
+    return out;
+}
 }
diff --git a/src/libutil/error.hh b/src/libutil/error.hh
index f402b692e68aa5c861ab2cce033f881b0b381349..8a48fa105b10cc40be426b25b4e846e790ecb2c6 100644
--- a/src/libutil/error.hh
+++ b/src/libutil/error.hh
@@ -1,121 +1,181 @@
-#ifndef error_hh
-#define error_hh
+#pragma once
 
-#include "ansicolor.hh"
-#include <string>
-#include <optional>
-#include <iostream>
+
+#include "ref.hh"
 #include "types.hh"
 
-namespace nix
-{
+#include <list>
+#include <memory>
+#include <map>
+#include <optional>
 
-typedef enum {
-    elWarning,
-    elError
-} ErrLevel;
+#include "fmt.hh"
 
-struct ErrPos
-{
-    int lineNumber;
-    int column;
-    string nixFile;
+/* Before 4.7, gcc's std::exception uses empty throw() specifiers for
+ * its (virtual) destructor and what() in c++11 mode, in violation of spec
+ */
+#ifdef __GNUC__
+#if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 7)
+#define EXCEPTION_NEEDS_THROW_SPEC
+#endif
+#endif
+
+namespace nix {
+
+/* 
+
+This file defines two main structs/classes used in nix error handling.
+
+ErrorInfo provides a standard payload of error information, with conversion to string
+happening in the logger rather than at the call site.
+
+BaseError is the ancestor of nix specific exceptions (and Interrupted), and contains
+an ErrorInfo.
+
+ErrorInfo structs are sent to the logger as part of an exception, or directly with the
+logError or logWarning macros.
 
+See the error-demo.cc program for usage examples.
+
+*/
+
+typedef enum {
+    lvlError = 0,
+    lvlWarn,
+    lvlInfo,
+    lvlTalkative,
+    lvlChatty,
+    lvlDebug,
+    lvlVomit
+} Verbosity;
+
+// ErrPos indicates the location of an error in a nix file.
+struct ErrPos {
+    int line = 0;
+    int column = 0;
+    string file;
+
+    operator bool() const
+    {
+        return line != 0;
+    }
+
+    // convert from the Pos struct, found in libexpr.
     template <class P>
     ErrPos& operator=(const P &pos)
     {
-        lineNumber = pos.line;
+        line = pos.line;
         column = pos.column;
-        nixFile = pos.file;
+        file = pos.file;
         return *this;
     }
 
     template <class P>
     ErrPos(const P &p)
     {
-      *this = p;
+        *this = p;
     }
 };
 
-struct NixCode
-{
+struct NixCode {
     ErrPos errPos;
     std::optional<string> prevLineOfCode;
-    string errLineOfCode;
+    std::optional<string> errLineOfCode;
     std::optional<string> nextLineOfCode;
 };
 
-// ----------------------------------------------------------------
-// format function for hints.  same as fmt, except templated values
-// are always in yellow.
+struct ErrorInfo {
+    Verbosity level;
+    string name;
+    string description;
+    std::optional<hintformat> hint;
+    std::optional<NixCode> nixCode;
 
-template <class T>
-struct yellowify
-{
-    yellowify(T &s) : value(s) {}
-    T &value;
+    static std::optional<string> programName;
 };
 
-template <class T>
-std::ostream& operator<<(std::ostream &out, const yellowify<T> &y)
-{
-    return out << ANSI_YELLOW << y.value << ANSI_NORMAL;
-}
+std::ostream& operator<<(std::ostream &out, const ErrorInfo &einfo);
 
-class hintformat
+/* BaseError should generally not be caught, as it has Interrupted as
+   a subclass. Catch Error instead. */
+class BaseError : public std::exception
 {
+protected:
+    string prefix_; // used for location traces etc.
+    mutable ErrorInfo err;
+
+    mutable std::optional<string> what_;
+    const string& calcWhat() const;
+    
 public:
-    hintformat(string format) :fmt(format)
-    {
-        fmt.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit);
-    }
-    template<class T>
-    hintformat& operator%(const T &value)
-    {
-        fmt % yellowify(value);
-        return *this;
-    }
+    unsigned int status = 1; // exit status
+
+    template<typename... Args>
+    BaseError(unsigned int status, const Args & ... args)
+        : err { .level = lvlError,
+                .hint = hintfmt(args...)
+              }
+        , status(status)
+    { }
+
+    template<typename... Args>
+    BaseError(const Args & ... args)
+        : err { .level = lvlError,
+                .hint = hintfmt(args...)
+              }
+    { }
+
+    BaseError(hintformat hint)
+        : err { .level = lvlError,
+                .hint = hint
+              }
+    { }
+
+    BaseError(ErrorInfo e)
+        : err(e)
+    { }
+
+    virtual const char* sname() const { return "BaseError"; }
+
+#ifdef EXCEPTION_NEEDS_THROW_SPEC
+    ~BaseError() throw () { };
+    const char * what() const throw () { return calcWhat().c_str(); }
+#else
+    const char * what() const noexcept override { return calcWhat().c_str(); }
+#endif
 
-    std::string str() const
-    {
-        return fmt.str();
-    }
+    const string & msg() const { return calcWhat(); }
+    const string & prefix() const { return prefix_; }
+    BaseError & addPrefix(const FormatOrString & fs);
 
-    template <typename U>
-    friend class AddHint;
-private:
-    format fmt;
+    const ErrorInfo & info() { calcWhat(); return err; }
 };
 
-std::ostream& operator<<(std::ostream &os, const hintformat &hf);
+#define MakeError(newClass, superClass) \
+    class newClass : public superClass                  \
+    {                                                   \
+    public:                                             \
+        using superClass::superClass;                   \
+        virtual const char* sname() const override { return #newClass; } \
+    }
 
-template<typename... Args>
-inline hintformat hintfmt(const std::string & fs, const Args & ... args)
-{
-    hintformat f(fs);
-    formatHelper(f, args...);
-    return f;
-}
+MakeError(Error, BaseError);
 
-// -------------------------------------------------
-// ErrorInfo.
-struct ErrorInfo
+class SysError : public Error
 {
-    ErrLevel level;
-    string name;
-    string description;
-    std::optional<hintformat> hint;
-    std::optional<NixCode> nixCode;
-
-    static std::optional<string> programName;
-};
+public:
+    int errNo;
 
-// --------------------------------------------------------
-// error printing
+    template<typename... Args>
+    SysError(const Args & ... args)
+      :Error("")
+    {
+        errNo = errno;
+        auto hf = hintfmt(args...);
+        err.hint = hintfmt("%1%: %2%", normaltxt(hf.str()), strerror(errNo));
+    }
 
-// just to cout for now.
-void printErrorInfo(const ErrorInfo &einfo);
+    virtual const char* sname() const override { return "SysError"; }
+};
 
 }
-
-#endif
diff --git a/src/libutil/fmt.hh b/src/libutil/fmt.hh
new file mode 100644
index 0000000000000000000000000000000000000000..12ab9c40718911c5ec152c91a2d3d11153364873
--- /dev/null
+++ b/src/libutil/fmt.hh
@@ -0,0 +1,139 @@
+#pragma once
+
+#include <boost/format.hpp>
+#include <string>
+#include "ansicolor.hh"
+
+
+namespace nix {
+
+
+/* Inherit some names from other namespaces for convenience. */
+using std::string;
+using boost::format;
+
+
+/* A variadic template that does nothing. Useful to call a function
+   for all variadic arguments but ignoring the result. */
+struct nop { template<typename... T> nop(T...) {} };
+
+
+struct FormatOrString
+{
+    string s;
+    FormatOrString(const string & s) : s(s) { };
+    template<class F>
+    FormatOrString(const F & f) : s(f.str()) { };
+    FormatOrString(const char * s) : s(s) { };
+};
+
+
+/* A helper for formatting strings. ‘fmt(format, a_0, ..., a_n)’ is
+   equivalent to ‘boost::format(format) % a_0 % ... %
+   ... a_n’. However, ‘fmt(s)’ is equivalent to ‘s’ (so no %-expansion
+   takes place). */
+
+template<class F>
+inline void formatHelper(F & f)
+{
+}
+
+template<class F, typename T, typename... Args>
+inline void formatHelper(F & f, const T & x, const Args & ... args)
+{
+    formatHelper(f % x, args...);
+}
+
+inline std::string fmt(const std::string & s)
+{
+    return s;
+}
+
+inline std::string fmt(const char * s)
+{
+    return s;
+}
+
+inline std::string fmt(const FormatOrString & fs)
+{
+    return fs.s;
+}
+
+template<typename... Args>
+inline std::string fmt(const std::string & fs, const Args & ... args)
+{
+    boost::format f(fs);
+    f.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit);
+    formatHelper(f, args...);
+    return f.str();
+}
+
+// -----------------------------------------------------------------------------
+// format function for hints in errors.  same as fmt, except templated values
+// are always in yellow.
+
+template <class T>
+struct yellowtxt
+{
+    yellowtxt(const T &s) : value(s) {}
+    const T &value;
+};
+
+template <class T>
+std::ostream& operator<<(std::ostream &out, const yellowtxt<T> &y)
+{
+    return out << ANSI_YELLOW << y.value << ANSI_NORMAL;
+}
+
+template <class T>
+struct normaltxt
+{
+    normaltxt(const T &s) : value(s) {}
+    const T &value;
+};
+
+template <class T>
+std::ostream& operator<<(std::ostream &out, const normaltxt<T> &y)
+{
+    return out << ANSI_NORMAL << y.value;
+}
+
+class hintformat
+{
+public:
+    hintformat(const string &format) :fmt(format)
+    {
+        fmt.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit);
+    }
+
+    hintformat(const hintformat &hf)
+    : fmt(hf.fmt)
+    {}
+
+    template<class T>
+    hintformat& operator%(const T &value)
+    {
+        fmt % yellowtxt(value);
+        return *this;
+    }
+
+    std::string str() const
+    {
+        return fmt.str();
+    }
+
+private:
+    format fmt;
+};
+
+std::ostream& operator<<(std::ostream &os, const hintformat &hf);
+
+template<typename... Args>
+inline hintformat hintfmt(const std::string & fs, const Args & ... args)
+{
+    hintformat f(fs);
+    formatHelper(f, args...);
+    return f;
+}
+
+}
diff --git a/src/libutil/logging.cc b/src/libutil/logging.cc
index 15cbc1589ead4cbbb3f170ad1eb5bd84b744d528..108dc3bd1070e9ff0c2c22d125b88441925b69c5 100644
--- a/src/libutil/logging.cc
+++ b/src/libutil/logging.cc
@@ -69,9 +69,17 @@ public:
         writeToStderr(prefix + filterANSIEscapes(fs.s, !tty) + "\n");
     }
 
+    void logEI(const ErrorInfo & ei) override
+    {
+        std::stringstream oss;
+        oss << ei;
+
+        log(ei.level, oss.str());
+    }
+
     void startActivity(ActivityId act, Verbosity lvl, ActivityType type,
         const std::string & s, const Fields & fields, ActivityId parent)
-        override
+    override
     {
         if (lvl <= verbosity && !s.empty())
             log(lvl, s + "...");
@@ -126,8 +134,7 @@ Activity::Activity(Logger & logger, Verbosity lvl, ActivityType type,
     logger.startActivity(id, lvl, type, s, fields, parent);
 }
 
-struct JSONLogger : Logger
-{
+struct JSONLogger : Logger {
     Logger & prevLogger;
 
     JSONLogger(Logger & prevLogger) : prevLogger(prevLogger) { }
@@ -163,6 +170,19 @@ struct JSONLogger : Logger
         write(json);
     }
 
+    void logEI(const ErrorInfo & ei) override
+    {
+        std::ostringstream oss;
+        oss << ei;
+
+        nlohmann::json json;
+        json["action"] = "msg";
+        json["level"] = ei.level;
+        json["msg"] = oss.str();
+
+        write(json);
+    }
+
     void startActivity(ActivityId act, Verbosity lvl, ActivityType type,
         const std::string & s, const Fields & fields, ActivityId parent) override
     {
@@ -253,13 +273,17 @@ bool handleJSONLogMessage(const std::string & msg,
         }
 
     } catch (std::exception & e) {
-        printError("bad log message from builder: %s", e.what());
+        logError({ 
+            .name = "Json log message",
+            .hint = hintfmt("bad log message from builder: %s", e.what())
+        });
     }
 
     return true;
 }
 
-Activity::~Activity() {
+Activity::~Activity()
+{
     try {
         logger.stopActivity(id);
     } catch (...) {
diff --git a/src/libutil/logging.hh b/src/libutil/logging.hh
index e3d91e01fb47faa76076f576cd856c6e0bffde6d..b99b246c34c9c2969218d744b07a39c1e0a1015d 100644
--- a/src/libutil/logging.hh
+++ b/src/libutil/logging.hh
@@ -1,19 +1,10 @@
 #pragma once
 
 #include "types.hh"
+#include "error.hh"
 
 namespace nix {
 
-typedef enum {
-    lvlError = 0,
-    lvlWarn,
-    lvlInfo,
-    lvlTalkative,
-    lvlChatty,
-    lvlDebug,
-    lvlVomit
-} Verbosity;
-
 typedef enum {
     actUnknown = 0,
     actCopyPath = 100,
@@ -75,6 +66,14 @@ public:
         log(lvlInfo, fs);
     }
 
+    virtual void logEI(const ErrorInfo &ei) = 0;
+
+    void logEI(Verbosity lvl, ErrorInfo ei)
+    {
+        ei.level = lvl;
+        logEI(ei);
+    }
+
     virtual void warn(const std::string & msg);
 
     virtual void startActivity(ActivityId act, Verbosity lvl, ActivityType type,
@@ -156,9 +155,23 @@ bool handleJSONLogMessage(const std::string & msg,
 
 extern Verbosity verbosity; /* suppress msgs > this */
 
-/* Print a message if the current log level is at least the specified
-   level. Note that this has to be implemented as a macro to ensure
-   that the arguments are evaluated lazily. */
+/* Print a message with the standard ErrorInfo format.
+   In general, use these 'log' macros for reporting problems that may require user
+   intervention or that need more explanation.  Use the 'print' macros for more
+   lightweight status messages. */
+#define logErrorInfo(level, errorInfo...) \
+    do { \
+        if (level <= nix::verbosity) { \
+            logger->logEI(level, errorInfo); \
+        } \
+    } while (0)
+
+#define logError(errorInfo...) logErrorInfo(lvlError, errorInfo)
+#define logWarning(errorInfo...) logErrorInfo(lvlWarn, errorInfo)
+
+/* Print a string message if the current log level is at least the specified
+   level. Note that this has to be implemented as a macro to ensure that the
+   arguments are evaluated lazily. */
 #define printMsg(level, args...) \
     do { \
         if (level <= nix::verbosity) { \
@@ -172,6 +185,7 @@ extern Verbosity verbosity; /* suppress msgs > this */
 #define debug(args...) printMsg(lvlDebug, args)
 #define vomit(args...) printMsg(lvlVomit, args)
 
+/* if verbosity >= lvlWarn, print a message with a yellow 'warning:' prefix. */
 template<typename... Args>
 inline void warn(const std::string & fs, const Args & ... args)
 {
diff --git a/src/libutil/serialise.cc b/src/libutil/serialise.cc
index 8201549fd7d070bddc3757263e3569c5953e804e..35f7ee917cd3d3be228680f2836e9e5e82afd76c 100644
--- a/src/libutil/serialise.cc
+++ b/src/libutil/serialise.cc
@@ -52,7 +52,10 @@ size_t threshold = 256 * 1024 * 1024;
 
 static void warnLargeDump()
 {
-    printError("warning: dumping very large path (> 256 MiB); this may run out of memory");
+    logWarning(ErrorInfo {
+        .name = "Large path",
+        .description = "dumping very large path (> 256 MiB); this may run out of memory"
+        });
 }
 
 
diff --git a/src/libutil/tests/local.mk b/src/libutil/tests/local.mk
index a297edb6478f1579b7e4a9245e96d5c8e56aace1..815e185600e01fc21bc2db23533fed0c7dd2ebbf 100644
--- a/src/libutil/tests/local.mk
+++ b/src/libutil/tests/local.mk
@@ -8,7 +8,7 @@ libutil-tests_INSTALL_DIR :=
 
 libutil-tests_SOURCES := $(wildcard $(d)/*.cc)
 
-libutil-tests_CXXFLAGS += -I src/libutil
+libutil-tests_CXXFLAGS += -I src/libutil -I src/libexpr
 
 libutil-tests_LIBS = libutil
 
diff --git a/src/libutil/tests/logging.cc b/src/libutil/tests/logging.cc
new file mode 100644
index 0000000000000000000000000000000000000000..fbdc91253b5c5f735651cab472c49045a5e840e0
--- /dev/null
+++ b/src/libutil/tests/logging.cc
@@ -0,0 +1,255 @@
+#include "logging.hh"
+#include "nixexpr.hh"
+#include "util.hh"
+
+#include <gtest/gtest.h>
+
+namespace nix {
+
+    /* ----------------------------------------------------------------------------
+     * logEI
+     * --------------------------------------------------------------------------*/
+
+    TEST(logEI, catpuresBasicProperties) {
+
+        MakeError(TestError, Error);
+        ErrorInfo::programName = std::optional("error-unit-test");
+
+        try {
+            throw TestError("an error for testing purposes");
+        } catch (Error &e) {
+            testing::internal::CaptureStderr();
+            logger->logEI(e.info());
+            auto str = testing::internal::GetCapturedStderr();
+
+            ASSERT_STREQ(str.c_str(),"\x1B[31;1merror:\x1B[0m\x1B[34;1m --- TestError ------------------------------------ error-unit-test\x1B[0m\nan error for testing purposes\n");
+        }
+    }
+
+    TEST(logEI, appendingHintsToPreviousError) {
+
+        MakeError(TestError, Error);
+        ErrorInfo::programName = std::optional("error-unit-test");
+
+        try {
+            auto e = Error("initial error");
+            throw TestError(e.info());
+        } catch (Error &e) {
+            ErrorInfo ei = e.info();
+            ei.hint = hintfmt("%s; subsequent error message.", normaltxt(e.info().hint ? e.info().hint->str() : ""));
+
+            testing::internal::CaptureStderr();
+            logger->logEI(ei);
+            auto str = testing::internal::GetCapturedStderr();
+
+            ASSERT_STREQ(str.c_str(), "\x1B[31;1merror:\x1B[0m\x1B[34;1m --- TestError ------------------------------------ error-unit-test\x1B[0m\n\x1B[33;1m\x1B[0minitial error\x1B[0m; subsequent error message.\n");
+        }
+
+    }
+
+    TEST(logEI, picksUpSysErrorExitCode) {
+
+        MakeError(TestError, Error);
+        ErrorInfo::programName = std::optional("error-unit-test");
+
+        try {
+            auto x = readFile(-1);
+        }
+        catch (SysError &e) {
+            testing::internal::CaptureStderr();
+            logError(e.info());
+            auto str = testing::internal::GetCapturedStderr();
+
+            ASSERT_STREQ(str.c_str(), "\x1B[31;1merror:\x1B[0m\x1B[34;1m --- SysError ------------------------------------- error-unit-test\x1B[0m\n\x1B[33;1m\x1B[0mstatting file\x1B[0m: \x1B[33;1mBad file descriptor\x1B[0m\n");
+
+        }
+    }
+
+    TEST(logEI, loggingErrorOnInfoLevel) {
+        testing::internal::CaptureStderr();
+
+        logger->logEI({ .level = lvlInfo,
+                .name = "Info name",
+                .description = "Info description",
+                });
+
+        auto str = testing::internal::GetCapturedStderr();
+        ASSERT_STREQ(str.c_str(), "\x1B[32;1minfo:\x1B[0m\x1B[34;1m --- Info name ------------------------------------- error-unit-test\x1B[0m\nInfo description\n");
+    }
+
+    TEST(logEI, loggingErrorOnTalkativeLevel) {
+        verbosity = lvlTalkative;
+
+        testing::internal::CaptureStderr();
+
+        logger->logEI({ .level = lvlTalkative,
+                        .name = "Talkative name",
+                        .description = "Talkative description",
+                        });
+
+        auto str = testing::internal::GetCapturedStderr();
+        ASSERT_STREQ(str.c_str(), "\x1B[32;1mtalk:\x1B[0m\x1B[34;1m --- Talkative name -------------------------------- error-unit-test\x1B[0m\nTalkative description\n");
+    }
+
+    TEST(logEI, loggingErrorOnChattyLevel) {
+        verbosity = lvlChatty;
+
+        testing::internal::CaptureStderr();
+
+        logger->logEI({ .level = lvlChatty,
+                        .name = "Chatty name",
+                        .description = "Talkative description",
+                        });
+
+        auto str = testing::internal::GetCapturedStderr();
+        ASSERT_STREQ(str.c_str(), "\x1B[32;1mchat:\x1B[0m\x1B[34;1m --- Chatty name ----------------------------------- error-unit-test\x1B[0m\nTalkative description\n");
+    }
+
+    TEST(logEI, loggingErrorOnDebugLevel) {
+        verbosity = lvlDebug;
+
+        testing::internal::CaptureStderr();
+
+        logger->logEI({ .level = lvlDebug,
+                        .name = "Debug name",
+                        .description = "Debug description",
+                        });
+
+        auto str = testing::internal::GetCapturedStderr();
+        ASSERT_STREQ(str.c_str(), "\x1B[33;1mdebug:\x1B[0m\x1B[34;1m --- Debug name ----------------------------------- error-unit-test\x1B[0m\nDebug description\n");
+    }
+
+    TEST(logEI, loggingErrorOnVomitLevel) {
+        verbosity = lvlVomit;
+
+        testing::internal::CaptureStderr();
+
+        logger->logEI({ .level = lvlVomit,
+                        .name = "Vomit name",
+                        .description = "Vomit description",
+                        });
+
+        auto str = testing::internal::GetCapturedStderr();
+        ASSERT_STREQ(str.c_str(), "\x1B[32;1mvomit:\x1B[0m\x1B[34;1m --- Vomit name ----------------------------------- error-unit-test\x1B[0m\nVomit description\n");
+    }
+
+    /* ----------------------------------------------------------------------------
+     * logError
+     * --------------------------------------------------------------------------*/
+
+
+    TEST(logError, logErrorWithoutHintOrCode) {
+        testing::internal::CaptureStderr();
+
+        logError({
+                .name = "name",
+                .description = "error description",
+                });
+
+        auto str = testing::internal::GetCapturedStderr();
+        ASSERT_STREQ(str.c_str(), "\x1B[31;1merror:\x1B[0m\x1B[34;1m --- name ----------------------------------------- error-unit-test\x1B[0m\nerror description\n");
+    }
+
+    TEST(logError, logErrorWithPreviousAndNextLinesOfCode) {
+        SymbolTable testTable;
+        auto problem_file = testTable.create("myfile.nix");
+
+        testing::internal::CaptureStderr();
+
+        logError({
+                .name = "error name",
+                .description = "error with code lines",
+                .hint = hintfmt("this hint has %1% templated %2%!!",
+                        "yellow",
+                        "values"),
+                .nixCode = NixCode {
+                .errPos = Pos(problem_file, 40, 13),
+                .prevLineOfCode = "previous line of code",
+                .errLineOfCode = "this is the problem line of code",
+                .nextLineOfCode = "next line of code",
+                }});
+
+
+        auto str = testing::internal::GetCapturedStderr();
+        ASSERT_STREQ(str.c_str(), "\x1B[31;1merror:\x1B[0m\x1B[34;1m --- error name ----------------------------------- error-unit-test\x1B[0m\nin file: \x1B[34;1mmyfile.nix (40:13)\x1B[0m\n\nerror with code lines\n\n    39| previous line of code\n    40| this is the problem line of code\n      |             \x1B[31;1m^\x1B[0m\n    41| next line of code\n\nthis hint has \x1B[33;1myellow\x1B[0m templated \x1B[33;1mvalues\x1B[0m!!\n");
+    }
+
+    TEST(logError, logErrorWithoutLinesOfCode) {
+        SymbolTable testTable;
+        auto problem_file = testTable.create("myfile.nix");
+        testing::internal::CaptureStderr();
+
+        logError({
+                .name = "error name",
+                .description = "error without any code lines.",
+                .hint = hintfmt("this hint has %1% templated %2%!!",
+                        "yellow",
+                        "values"),
+                .nixCode = NixCode {
+                .errPos = Pos(problem_file, 40, 13)
+                }});
+
+        auto str = testing::internal::GetCapturedStderr();
+        ASSERT_STREQ(str.c_str(), "\x1B[31;1merror:\x1B[0m\x1B[34;1m --- error name ----------------------------------- error-unit-test\x1B[0m\nin file: \x1B[34;1mmyfile.nix (40:13)\x1B[0m\n\nerror without any code lines.\n\nthis hint has \x1B[33;1myellow\x1B[0m templated \x1B[33;1mvalues\x1B[0m!!\n");
+    }
+
+    TEST(logError, logErrorWithOnlyHintAndName) {
+        SymbolTable testTable;
+        auto problem_file = testTable.create("myfile.nix");
+        testing::internal::CaptureStderr();
+
+        logError({
+                .name = "error name",
+                .hint = hintfmt("hint %1%", "only"),
+                .nixCode = NixCode {
+                .errPos = Pos(problem_file, 40, 13)
+                }});
+
+        auto str = testing::internal::GetCapturedStderr();
+        ASSERT_STREQ(str.c_str(), "\x1B[31;1merror:\x1B[0m\x1B[34;1m --- error name ----------------------------------- error-unit-test\x1B[0m\nin file: \x1B[34;1mmyfile.nix (40:13)\x1B[0m\n\nhint \x1B[33;1monly\x1B[0m\n");
+
+    }
+
+    /* ----------------------------------------------------------------------------
+     * logWarning
+     * --------------------------------------------------------------------------*/
+
+    TEST(logWarning, logWarningWithNameDescriptionAndHint) {
+        testing::internal::CaptureStderr();
+
+        logWarning({
+            .name = "name",
+            .description = "error description",
+            .hint = hintfmt("there was a %1%", "warning"),
+        });
+
+        auto str = testing::internal::GetCapturedStderr();
+        ASSERT_STREQ(str.c_str(), "\x1B[33;1mwarning:\x1B[0m\x1B[34;1m --- name --------------------------------------- error-unit-test\x1B[0m\nerror description\n\nthere was a \x1B[33;1mwarning\x1B[0m\n");
+    }
+
+    TEST(logWarning, logWarningWithFileLineNumAndCode) {
+
+        SymbolTable testTable;
+        auto problem_file = testTable.create("myfile.nix");
+
+        testing::internal::CaptureStderr();
+
+        logWarning({
+                .name = "warning name",
+                .description = "warning description",
+                .hint = hintfmt("this hint has %1% templated %2%!!",
+                        "yellow",
+                        "values"),
+                .nixCode = NixCode {
+                .errPos = Pos(problem_file, 40, 13),
+                .prevLineOfCode = std::nullopt,
+                .errLineOfCode = "this is the problem line of code",
+                .nextLineOfCode = std::nullopt
+                }});
+
+
+        auto str = testing::internal::GetCapturedStderr();
+        ASSERT_STREQ(str.c_str(), "\x1B[33;1mwarning:\x1B[0m\x1B[34;1m --- warning name ------------------------------- error-unit-test\x1B[0m\nin file: \x1B[34;1mmyfile.nix (40:13)\x1B[0m\n\nwarning description\n\n    40| this is the problem line of code\n      |             \x1B[31;1m^\x1B[0m\n\nthis hint has \x1B[33;1myellow\x1B[0m templated \x1B[33;1mvalues\x1B[0m!!\n");
+    }
+
+}
diff --git a/src/libutil/types.hh b/src/libutil/types.hh
index 250c9581dd6d4075e468a9eb8166a86236022a5b..89ae108f9cb45a6c4f5075325f5cba200370a957 100644
--- a/src/libutil/types.hh
+++ b/src/libutil/types.hh
@@ -3,157 +3,24 @@
 
 #include "ref.hh"
 
-#include <string>
 #include <list>
 #include <set>
-#include <memory>
 #include <map>
-
-#include <boost/format.hpp>
-
-/* Before 4.7, gcc's std::exception uses empty throw() specifiers for
- * its (virtual) destructor and what() in c++11 mode, in violation of spec
- */
-#ifdef __GNUC__
-#if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 7)
-#define EXCEPTION_NEEDS_THROW_SPEC
-#endif
-#endif
-
+#include <vector>
 
 namespace nix {
 
-
-/* Inherit some names from other namespaces for convenience. */
-using std::string;
 using std::list;
 using std::set;
 using std::vector;
-using boost::format;
-
-
-/* A variadic template that does nothing. Useful to call a function
-   for all variadic arguments but ignoring the result. */
-struct nop { template<typename... T> nop(T...) {} };
-
-
-struct FormatOrString
-{
-    string s;
-    FormatOrString(const string & s) : s(s) { };
-    template<class F>
-    FormatOrString(const F & f) : s(f.str()) { };
-    FormatOrString(const char * s) : s(s) { };
-};
-
-
-/* A helper for formatting strings. ‘fmt(format, a_0, ..., a_n)’ is
-   equivalent to ‘boost::format(format) % a_0 % ... %
-   ... a_n’. However, ‘fmt(s)’ is equivalent to ‘s’ (so no %-expansion
-   takes place). */
-
-template<class F>
-inline void formatHelper(F & f)
-{
-}
-
-template<class F, typename T, typename... Args>
-inline void formatHelper(F & f, const T & x, const Args & ... args)
-{
-    formatHelper(f % x, args...);
-}
-
-inline std::string fmt(const std::string & s)
-{
-    return s;
-}
-
-inline std::string fmt(const char * s)
-{
-    return s;
-}
-
-inline std::string fmt(const FormatOrString & fs)
-{
-    return fs.s;
-}
-
-template<typename... Args>
-inline std::string fmt(const std::string & fs, const Args & ... args)
-{
-    boost::format f(fs);
-    f.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit);
-    formatHelper(f, args...);
-    return f.str();
-}
-
-
-/* BaseError should generally not be caught, as it has Interrupted as
-   a subclass. Catch Error instead. */
-class BaseError : public std::exception
-{
-protected:
-    string prefix_; // used for location traces etc.
-    string err;
-public:
-    unsigned int status = 1; // exit status
-
-    template<typename... Args>
-    BaseError(unsigned int status, const Args & ... args)
-        : err(fmt(args...))
-        , status(status)
-    {
-    }
-
-    template<typename... Args>
-    BaseError(const Args & ... args)
-        : err(fmt(args...))
-    {
-    }
-
-#ifdef EXCEPTION_NEEDS_THROW_SPEC
-    ~BaseError() throw () { };
-    const char * what() const throw () { return err.c_str(); }
-#else
-    const char * what() const noexcept { return err.c_str(); }
-#endif
-
-    const string & msg() const { return err; }
-    const string & prefix() const { return prefix_; }
-    BaseError & addPrefix(const FormatOrString & fs);
-};
-
-#define MakeError(newClass, superClass) \
-    class newClass : public superClass                  \
-    {                                                   \
-    public:                                             \
-        using superClass::superClass;                   \
-    }
-
-MakeError(Error, BaseError);
-
-class SysError : public Error
-{
-public:
-    int errNo;
-
-    template<typename... Args>
-    SysError(const Args & ... args)
-        : Error(addErrno(fmt(args...)))
-    { }
-
-private:
-
-    std::string addErrno(const std::string & s);
-};
-
+using std::string;
 
 typedef list<string> Strings;
 typedef set<string> StringSet;
-typedef std::map<std::string, std::string> StringMap;
-
+typedef std::map<string, string> StringMap;
 
 /* Paths are just strings. */
+
 typedef string Path;
 typedef list<Path> Paths;
 typedef set<Path> PathSet;
diff --git a/src/libutil/url.hh b/src/libutil/url.hh
index 4a0d4071ba6528946c8044d494b0f1d05aeacb4c..2ef88ef2a1ed28f5c86049f2e582e6cf5e73e6a3 100644
--- a/src/libutil/url.hh
+++ b/src/libutil/url.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "types.hh"
+#include "error.hh"
 
 #include <regex>
 
diff --git a/src/libutil/util.cc b/src/libutil/util.cc
index b66447e08ccd4100ae61450b61c53912e729d1f8..a2281237b222cf1a8376779d9f7588ee1ae0d2a4 100644
--- a/src/libutil/util.cc
+++ b/src/libutil/util.cc
@@ -40,24 +40,6 @@ extern char * * environ;
 
 namespace nix {
 
-
-const std::string nativeSystem = SYSTEM;
-
-
-BaseError & BaseError::addPrefix(const FormatOrString & fs)
-{
-    prefix_ = fs.s + prefix_;
-    return *this;
-}
-
-
-std::string SysError::addErrno(const std::string & s)
-{
-    errNo = errno;
-    return s + ": " + strerror(errNo);
-}
-
-
 std::optional<std::string> getEnv(const std::string & key)
 {
     char * value = getenv(key.c_str());
@@ -129,7 +111,7 @@ Path canonPath(const Path & path, bool resolveSymlinks)
     string s;
 
     if (path[0] != '/')
-        throw Error(format("not an absolute path: '%1%'") % path);
+        throw Error("not an absolute path: '%1%'", path);
 
     string::const_iterator i = path.begin(), end = path.end();
     string temp;
@@ -165,7 +147,7 @@ Path canonPath(const Path & path, bool resolveSymlinks)
                the symlink target might contain new symlinks). */
             if (resolveSymlinks && isLink(s)) {
                 if (++followCount >= maxFollow)
-                    throw Error(format("infinite symlink recursion in path '%1%'") % path);
+                    throw Error("infinite symlink recursion in path '%1%'", path);
                 temp = absPath(readLink(s), dirOf(s))
                     + string(i, end);
                 i = temp.begin(); /* restart */
@@ -226,7 +208,7 @@ struct stat lstat(const Path & path)
 {
     struct stat st;
     if (lstat(path.c_str(), &st))
-        throw SysError(format("getting status of '%1%'") % path);
+        throw SysError("getting status of '%1%'", path);
     return st;
 }
 
@@ -238,7 +220,7 @@ bool pathExists(const Path & path)
     res = lstat(path.c_str(), &st);
     if (!res) return true;
     if (errno != ENOENT && errno != ENOTDIR)
-        throw SysError(format("getting status of %1%") % path);
+        throw SysError("getting status of %1%", path);
     return false;
 }
 
@@ -286,7 +268,7 @@ DirEntries readDirectory(DIR *dir, const Path & path)
 #endif
         );
     }
-    if (errno) throw SysError(format("reading directory '%1%'") % path);
+    if (errno) throw SysError("reading directory '%1%'", path);
 
     return entries;
 }
@@ -294,7 +276,7 @@ DirEntries readDirectory(DIR *dir, const Path & path)
 DirEntries readDirectory(const Path & path)
 {
     AutoCloseDir dir(opendir(path.c_str()));
-    if (!dir) throw SysError(format("opening directory '%1%'") % path);
+    if (!dir) throw SysError("opening directory '%1%'", path);
 
     return readDirectory(dir.get(), path);
 }
@@ -324,7 +306,7 @@ string readFile(const Path & path)
 {
     AutoCloseFD fd = open(path.c_str(), O_RDONLY | O_CLOEXEC);
     if (!fd)
-        throw SysError(format("opening file '%1%'") % path);
+        throw SysError("opening file '%1%'", path);
     return readFile(fd.get());
 }
 
@@ -332,7 +314,8 @@ string readFile(const Path & path)
 void readFile(const Path & path, Sink & sink)
 {
     AutoCloseFD fd = open(path.c_str(), O_RDONLY | O_CLOEXEC);
-    if (!fd) throw SysError("opening file '%s'", path);
+    if (!fd)  
+        throw SysError("opening file '%s'", path);
     drainFD(fd.get(), sink);
 }
 
@@ -341,7 +324,7 @@ void writeFile(const Path & path, const string & s, mode_t mode)
 {
     AutoCloseFD fd = open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode);
     if (!fd)
-        throw SysError(format("opening file '%1%'") % path);
+        throw SysError("opening file '%1%'", path);
     writeFull(fd.get(), s);
 }
 
@@ -350,7 +333,7 @@ void writeFile(const Path & path, Source & source, mode_t mode)
 {
     AutoCloseFD fd = open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode);
     if (!fd)
-        throw SysError(format("opening file '%1%'") % path);
+        throw SysError("opening file '%1%'", path);
 
     std::vector<unsigned char> buf(64 * 1024);
 
@@ -400,7 +383,7 @@ static void _deletePath(int parentfd, const Path & path, unsigned long long & by
     struct stat st;
     if (fstatat(parentfd, name.c_str(), &st, AT_SYMLINK_NOFOLLOW) == -1) {
         if (errno == ENOENT) return;
-        throw SysError(format("getting status of '%1%'") % path);
+        throw SysError("getting status of '%1%'", path);
     }
 
     if (!S_ISDIR(st.st_mode) && st.st_nlink == 1)
@@ -411,15 +394,15 @@ static void _deletePath(int parentfd, const Path & path, unsigned long long & by
         const auto PERM_MASK = S_IRUSR | S_IWUSR | S_IXUSR;
         if ((st.st_mode & PERM_MASK) != PERM_MASK) {
             if (fchmodat(parentfd, name.c_str(), st.st_mode | PERM_MASK, 0) == -1)
-                throw SysError(format("chmod '%1%'") % path);
+                throw SysError("chmod '%1%'", path);
         }
 
         int fd = openat(parentfd, path.c_str(), O_RDONLY);
         if (!fd)
-            throw SysError(format("opening directory '%1%'") % path);
+            throw SysError("opening directory '%1%'", path);
         AutoCloseDir dir(fdopendir(fd));
         if (!dir)
-            throw SysError(format("opening directory '%1%'") % path);
+            throw SysError("opening directory '%1%'", path);
         for (auto & i : readDirectory(dir.get(), path))
             _deletePath(dirfd(dir.get()), path + "/" + i.name, bytesFreed);
     }
@@ -427,7 +410,7 @@ static void _deletePath(int parentfd, const Path & path, unsigned long long & by
     int flags = S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0;
     if (unlinkat(parentfd, name.c_str(), flags) == -1) {
         if (errno == ENOENT) return;
-        throw SysError(format("cannot unlink '%1%'") % path);
+        throw SysError("cannot unlink '%1%'", path);
     }
 }
 
@@ -443,7 +426,7 @@ static void _deletePath(const Path & path, unsigned long long & bytesFreed)
         // for backwards compatibility.
         if (errno == ENOENT) return;
 
-        throw SysError(format("opening directory '%1%'") % path);
+        throw SysError("opening directory '%1%'", path);
     }
 
     _deletePath(dirfd.get(), path, bytesFreed);
@@ -497,12 +480,12 @@ Path createTempDir(const Path & tmpRoot, const Path & prefix,
                "wheel", then "tar" will fail to unpack archives that
                have the setgid bit set on directories. */
             if (chown(tmpDir.c_str(), (uid_t) -1, getegid()) != 0)
-                throw SysError(format("setting group of directory '%1%'") % tmpDir);
+                throw SysError("setting group of directory '%1%'", tmpDir);
 #endif
             return tmpDir;
         }
         if (errno != EEXIST)
-            throw SysError(format("creating directory '%1%'") % tmpDir);
+            throw SysError("creating directory '%1%'", tmpDir);
     }
 }
 
@@ -584,15 +567,15 @@ Paths createDirs(const Path & path)
     if (lstat(path.c_str(), &st) == -1) {
         created = createDirs(dirOf(path));
         if (mkdir(path.c_str(), 0777) == -1 && errno != EEXIST)
-            throw SysError(format("creating directory '%1%'") % path);
+            throw SysError("creating directory '%1%'", path);
         st = lstat(path);
         created.push_back(path);
     }
 
     if (S_ISLNK(st.st_mode) && stat(path.c_str(), &st) == -1)
-        throw SysError(format("statting symlink '%1%'") % path);
+        throw SysError("statting symlink '%1%'", path);
 
-    if (!S_ISDIR(st.st_mode)) throw Error(format("'%1%' is not a directory") % path);
+    if (!S_ISDIR(st.st_mode)) throw Error("'%1%' is not a directory", path);
 
     return created;
 }
@@ -601,7 +584,7 @@ Paths createDirs(const Path & path)
 void createSymlink(const Path & target, const Path & link)
 {
     if (symlink(target.c_str(), link.c_str()))
-        throw SysError(format("creating symlink from '%1%' to '%2%'") % link % target);
+        throw SysError("creating symlink from '%1%' to '%2%'", link, target);
 }
 
 
@@ -618,7 +601,7 @@ void replaceSymlink(const Path & target, const Path & link)
         }
 
         if (rename(tmp.c_str(), link.c_str()) != 0)
-            throw SysError(format("renaming '%1%' to '%2%'") % tmp % link);
+            throw SysError("renaming '%1%' to '%2%'", tmp, link);
 
         break;
     }
@@ -723,7 +706,7 @@ AutoDelete::~AutoDelete()
                 deletePath(path);
             else {
                 if (remove(path.c_str()) == -1)
-                    throw SysError(format("cannot unlink '%1%'") % path);
+                    throw SysError("cannot unlink '%1%'", path);
             }
         }
     } catch (...) {
@@ -789,7 +772,7 @@ void AutoCloseFD::close()
     if (fd != -1) {
         if (::close(fd) == -1)
             /* This should never happen. */
-            throw SysError(format("closing file descriptor %1%") % fd);
+            throw SysError("closing file descriptor %1%", fd);
     }
 }
 
@@ -862,7 +845,7 @@ int Pid::kill()
 {
     assert(pid != -1);
 
-    debug(format("killing process %1%") % pid);
+    debug("killing process %1%", pid);
 
     /* Send the requested signal to the child.  If it has its own
        process group, send the signal to every process in the child
@@ -874,7 +857,7 @@ int Pid::kill()
 #if __FreeBSD__ || __APPLE__
         if (errno != EPERM || ::kill(pid, 0) != 0)
 #endif
-            printError((SysError("killing process %d", pid).msg()));
+            logError(SysError("killing process %d", pid).info());
     }
 
     return wait();
@@ -920,7 +903,7 @@ pid_t Pid::release()
 
 void killUser(uid_t uid)
 {
-    debug(format("killing all processes running under uid '%1%'") % uid);
+    debug("killing all processes running under uid '%1%'", uid);
 
     assert(uid != 0); /* just to be safe... */
 
@@ -949,7 +932,7 @@ void killUser(uid_t uid)
 #endif
             if (errno == ESRCH) break; /* no more processes */
             if (errno != EINTR)
-                throw SysError(format("cannot kill processes for uid '%1%'") % uid);
+                throw SysError("cannot kill processes for uid '%1%'", uid);
         }
 
         _exit(0);
@@ -957,7 +940,7 @@ void killUser(uid_t uid)
 
     int status = pid.wait();
     if (status != 0)
-        throw Error(format("cannot kill processes for uid '%1%': %2%") % uid % statusToString(status));
+        throw Error("cannot kill processes for uid '%1%': %2%", uid, statusToString(status));
 
     /* !!! We should really do some check to make sure that there are
        no processes left running under `uid', but there is no portable
@@ -1351,7 +1334,7 @@ void ignoreException()
     try {
         throw;
     } catch (std::exception & e) {
-        printError(format("error (ignored): %1%") % e.what());
+        printError("error (ignored): %1%", e.what());
     }
 }
 
@@ -1464,17 +1447,6 @@ string base64Decode(std::string_view s)
 }
 
 
-void callFailure(const std::function<void(std::exception_ptr exc)> & failure, std::exception_ptr exc)
-{
-    try {
-        failure(exc);
-    } catch (std::exception & e) {
-        printError(format("uncaught exception: %s") % e.what());
-        abort();
-    }
-}
-
-
 static Sync<std::pair<unsigned short, unsigned short>> windowSize{{0, 0}};
 
 
diff --git a/src/libutil/util.hh b/src/libutil/util.hh
index c95232317c3b812a414598b184ed365c5e367873..3641daaec8c730efb52b0a80ff8248ab33535f88 100644
--- a/src/libutil/util.hh
+++ b/src/libutil/util.hh
@@ -1,6 +1,7 @@
 #pragma once
 
 #include "types.hh"
+#include "error.hh"
 #include "logging.hh"
 #include "ansicolor.hh"
 
diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc
index 8649de5e9e8839599f81f8fb01941924142aa50b..591fff99980795a0820ebd744427565c0b9d03b7 100755
--- a/src/nix-build/nix-build.cc
+++ b/src/nix-build/nix-build.cc
@@ -368,7 +368,12 @@ static void _main(int argc, char * * argv)
                 shell = drv->queryOutPath() + "/bin/bash";
 
             } catch (Error & e) {
-                printError("warning: %s; will use bash from your environment", e.what());
+                logWarning(
+                    ErrorInfo {
+                        .name = "bashInteractive",
+                        .hint = hintfmt("%s; will use bash from your environment", 
+                            (e.info().hint ? e.info().hint->str() : ""))
+                    });
                 shell = "bash";
             }
         }
diff --git a/src/nix-channel/nix-channel.cc b/src/nix-channel/nix-channel.cc
index abd390414830ab5c678611c0b95daf8911a978bd..3ccf620c946476be398fd0d5a1aa7c7aedac62fe 100755
--- a/src/nix-channel/nix-channel.cc
+++ b/src/nix-channel/nix-channel.cc
@@ -38,7 +38,7 @@ static void writeChannels()
 {
     auto channelsFD = AutoCloseFD{open(channelsList.c_str(), O_WRONLY | O_CLOEXEC | O_CREAT | O_TRUNC, 0644)};
     if (!channelsFD)
-        throw SysError(format("opening '%1%' for writing") % channelsList);
+        throw SysError("opening '%1%' for writing", channelsList);
     for (const auto & channel : channels)
         writeFull(channelsFD.get(), channel.second + " " + channel.first + "\n");
 }
@@ -47,9 +47,9 @@ static void writeChannels()
 static void addChannel(const string & url, const string & name)
 {
     if (!regex_search(url, std::regex("^(file|http|https)://")))
-        throw Error(format("invalid channel URL '%1%'") % url);
+        throw Error("invalid channel URL '%1%'", url);
     if (!regex_search(name, std::regex("^[a-zA-Z0-9_][a-zA-Z0-9_\\.-]*$")))
-        throw Error(format("invalid channel identifier '%1%'") % name);
+        throw Error("invalid channel identifier '%1%'", name);
     readChannels();
     channels[name] = url;
     writeChannels();
@@ -137,9 +137,9 @@ static void update(const StringSet & channelNames)
         if (S_ISLNK(st.st_mode))
             // old-skool ~/.nix-defexpr
             if (unlink(nixDefExpr.c_str()) == -1)
-                throw SysError(format("unlinking %1%") % nixDefExpr);
+                throw SysError("unlinking %1%", nixDefExpr);
     } else if (errno != ENOENT) {
-        throw SysError(format("getting status of %1%") % nixDefExpr);
+        throw SysError("getting status of %1%", nixDefExpr);
     }
     createDirs(nixDefExpr);
     auto channelLink = nixDefExpr + "/channels";
diff --git a/src/nix-daemon/nix-daemon.cc b/src/nix-daemon/nix-daemon.cc
index e68d1b1bed6ebccf650f6fbac1bdb49709078196..582c78d14ed3fadce70558007631d32582ec001e 100644
--- a/src/nix-daemon/nix-daemon.cc
+++ b/src/nix-daemon/nix-daemon.cc
@@ -36,7 +36,7 @@ using namespace nix::daemon;
 #define SPLICE_F_MOVE 0
 static ssize_t splice(int fd_in, void *off_in, int fd_out, void *off_out, size_t len, unsigned int flags)
 {
-    /* We ignore most parameters, we just have them for conformance with the linux syscall */
+    // We ignore most parameters, we just have them for conformance with the linux syscall 
     std::vector<char> buf(8192);
     auto read_count = read(fd_in, buf.data(), buf.size());
     if (read_count == -1)
@@ -57,7 +57,7 @@ static void sigChldHandler(int sigNo)
 {
     // Ensure we don't modify errno of whatever we've interrupted
     auto saved_errno = errno;
-    /* Reap all dead children. */
+    //  Reap all dead children. 
     while (waitpid(-1, 0, WNOHANG) > 0) ;
     errno = saved_errno;
 }
@@ -106,7 +106,7 @@ struct PeerInfo
 };
 
 
-/* Get the identity of the caller, if possible. */
+//  Get the identity of the caller, if possible. 
 static PeerInfo getPeerInfo(int remote)
 {
     PeerInfo peer = { false, 0, false, 0, false, 0 };
@@ -154,13 +154,12 @@ static void daemonLoop(char * * argv)
     if (chdir("/") == -1)
         throw SysError("cannot change current directory");
 
-    /* Get rid of children automatically; don't let them become
-       zombies. */
+    //  Get rid of children automatically; don't let them become zombies. 
     setSigChldAction(true);
 
     AutoCloseFD fdSocket;
 
-    /* Handle socket-based activation by systemd. */
+    //  Handle socket-based activation by systemd. 
     auto listenFds = getEnv("LISTEN_FDS");
     if (listenFds) {
         if (getEnv("LISTEN_PID") != std::to_string(getpid()) || listenFds != "1")
@@ -169,17 +168,17 @@ static void daemonLoop(char * * argv)
         closeOnExec(fdSocket.get());
     }
 
-    /* Otherwise, create and bind to a Unix domain socket. */
+    //  Otherwise, create and bind to a Unix domain socket. 
     else {
         createDirs(dirOf(settings.nixDaemonSocketFile));
         fdSocket = createUnixDomainSocket(settings.nixDaemonSocketFile, 0666);
     }
 
-    /* Loop accepting connections. */
+    //  Loop accepting connections. 
     while (1) {
 
         try {
-            /* Accept a connection. */
+            //  Accept a connection. 
             struct sockaddr_un remoteAddr;
             socklen_t remoteAddrLen = sizeof(remoteAddr);
 
@@ -209,13 +208,13 @@ static void daemonLoop(char * * argv)
                 trusted = Trusted;
 
             if ((!trusted && !matchUser(user, group, allowedUsers)) || group == settings.buildUsersGroup)
-                throw Error(format("user '%1%' is not allowed to connect to the Nix daemon") % user);
+                throw Error("user '%1%' is not allowed to connect to the Nix daemon", user);
 
             printInfo(format((string) "accepted connection from pid %1%, user %2%" + (trusted ? " (trusted)" : ""))
                 % (peer.pidKnown ? std::to_string(peer.pid) : "<unknown>")
                 % (peer.uidKnown ? user : "<unknown>"));
 
-            /* Fork a child to handle the connection. */
+            //  Fork a child to handle the connection. 
             ProcessOptions options;
             options.errorPrefix = "unexpected Nix daemon error: ";
             options.dieWithParent = false;
@@ -224,20 +223,20 @@ static void daemonLoop(char * * argv)
             startProcess([&]() {
                 fdSocket = -1;
 
-                /* Background the daemon. */
+                //  Background the daemon. 
                 if (setsid() == -1)
-                    throw SysError(format("creating a new session"));
+                    throw SysError("creating a new session");
 
-                /* Restore normal handling of SIGCHLD. */
+                //  Restore normal handling of SIGCHLD. 
                 setSigChldAction(false);
 
-                /* For debugging, stuff the pid into argv[1]. */
+                //  For debugging, stuff the pid into argv[1]. 
                 if (peer.pidKnown && argv[1]) {
                     string processName = std::to_string(peer.pid);
                     strncpy(argv[1], processName.c_str(), strlen(argv[1]));
                 }
 
-                /* Handle the connection. */
+                //  Handle the connection. 
                 FdSource from(remote.get());
                 FdSink to(remote.get());
                 processConnection(openUncachedStore(), from, to, trusted, NotRecursive, user, peer.uid);
@@ -247,8 +246,11 @@ static void daemonLoop(char * * argv)
 
         } catch (Interrupted & e) {
             return;
-        } catch (Error & e) {
-            printError(format("error processing connection: %1%") % e.msg());
+        } catch (Error & error) {
+            ErrorInfo ei = error.info();
+            ei.hint = std::optional(hintfmt("error processing connection: %1%",
+                (error.info().hint.has_value() ? error.info().hint->str() : "")));
+            logError(ei);
         }
     }
 }
@@ -261,7 +263,7 @@ static int _main(int argc, char * * argv)
 
         parseCmdLine(argc, argv, [&](Strings::iterator & arg, const Strings::iterator & end) {
             if (*arg == "--daemon")
-                ; /* ignored for backwards compatibility */
+                ; //  ignored for backwards compatibility 
             else if (*arg == "--help")
                 showManPage("nix-daemon");
             else if (*arg == "--version")
@@ -276,7 +278,7 @@ static int _main(int argc, char * * argv)
 
         if (stdio) {
             if (getStoreType() == tDaemon) {
-                /* Forward on this connection to the real daemon */
+                //  Forward on this connection to the real daemon 
                 auto socketPath = settings.nixDaemonSocketFile;
                 auto s = socket(PF_UNIX, SOCK_STREAM, 0);
                 if (s == -1)
@@ -284,17 +286,17 @@ static int _main(int argc, char * * argv)
 
                 auto socketDir = dirOf(socketPath);
                 if (chdir(socketDir.c_str()) == -1)
-                    throw SysError(format("changing to socket directory '%1%'") % socketDir);
+                    throw SysError("changing to socket directory '%1%'", socketDir);
 
                 auto socketName = std::string(baseNameOf(socketPath));
                 auto addr = sockaddr_un{};
                 addr.sun_family = AF_UNIX;
                 if (socketName.size() + 1 >= sizeof(addr.sun_path))
-                    throw Error(format("socket name %1% is too long") % socketName);
+                    throw Error("socket name %1% is too long", socketName);
                 strcpy(addr.sun_path, socketName.c_str());
 
                 if (connect(s, (struct sockaddr *) &addr, sizeof(addr)) == -1)
-                    throw SysError(format("cannot connect to daemon at %1%") % socketPath);
+                    throw SysError("cannot connect to daemon at %1%", socketPath);
 
                 auto nfds = (s > STDIN_FILENO ? s : STDIN_FILENO) + 1;
                 while (true) {
diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc
index f7b04eb2bfa37bbf294c08cf32008739b85293b7..7ab5ba5000c7933347043955ad524a7488402a27 100644
--- a/src/nix-env/nix-env.cc
+++ b/src/nix-env/nix-env.cc
@@ -25,7 +25,6 @@
 #include <sys/stat.h>
 #include <unistd.h>
 
-
 using namespace nix;
 using std::cout;
 
@@ -70,8 +69,7 @@ typedef void (* Operation) (Globals & globals,
 static string needArg(Strings::iterator & i,
     Strings & args, const string & arg)
 {
-    if (i == args.end()) throw UsageError(
-        format("'%1%' requires an argument") % arg);
+    if (i == args.end()) throw UsageError("'%1%' requires an argument", arg);
     return *i++;
 }
 
@@ -125,7 +123,10 @@ static void getAllExprs(EvalState & state,
             if (hasSuffix(attrName, ".nix"))
                 attrName = string(attrName, 0, attrName.size() - 4);
             if (!attrs.insert(attrName).second) {
-                printError(format("warning: name collision in input Nix expressions, skipping '%1%'") % path2);
+                logError({ 
+                    .name = "Name collision",
+                    .hint = hintfmt("warning: name collision in input Nix expressions, skipping '%1%'", path2)
+                });
                 continue;
             }
             /* Load the expression on demand. */
@@ -133,7 +134,7 @@ static void getAllExprs(EvalState & state,
             Value & vArg(*state.allocValue());
             mkString(vArg, path2);
             if (v.attrs->size() == v.attrs->capacity())
-                throw Error(format("too many Nix expressions in directory '%1%'") % path);
+                throw Error("too many Nix expressions in directory '%1%'", path);
             mkApp(*state.allocAttr(v, state.symbols.create(attrName)), vFun, vArg);
         }
         else if (S_ISDIR(st.st_mode))
@@ -144,11 +145,12 @@ static void getAllExprs(EvalState & state,
 }
 
 
+
 static void loadSourceExpr(EvalState & state, const Path & path, Value & v)
 {
     struct stat st;
     if (stat(path.c_str(), &st) == -1)
-        throw SysError(format("getting information about '%1%'") % path);
+        throw SysError("getting information about '%1%'", path);
 
     if (isNixExpr(path, st))
         state.evalFile(path, v);
@@ -221,7 +223,7 @@ static void checkSelectorUse(DrvNames & selectors)
     /* Check that all selectors have been used. */
     for (auto & i : selectors)
         if (i.hits == 0 && i.fullName != "*")
-            throw Error(format("selector '%1%' matches no derivations") % i.fullName);
+            throw Error("selector '%1%' matches no derivations", i.fullName);
 }
 
 
@@ -507,7 +509,7 @@ static void opInstall(Globals & globals, Strings opFlags, Strings opArgs)
             globals.preserveInstalled = true;
         else if (arg == "--remove-all" || arg == "-r")
             globals.removeAll = true;
-        else throw UsageError(format("unknown flag '%1%'") % arg);
+        else throw UsageError("unknown flag '%1%'", arg);
     }
 
     installDerivations(globals, opArgs, globals.profile);
@@ -618,7 +620,7 @@ static void opUpgrade(Globals & globals, Strings opFlags, Strings opArgs)
         else if (arg == "--leq") upgradeType = utLeq;
         else if (arg == "--eq") upgradeType = utEq;
         else if (arg == "--always") upgradeType = utAlways;
-        else throw UsageError(format("unknown flag '%1%'") % arg);
+        else throw UsageError("unknown flag '%1%'", arg);
     }
 
     upgradeDerivations(globals, opArgs, upgradeType);
@@ -637,7 +639,7 @@ static void setMetaFlag(EvalState & state, DrvInfo & drv,
 static void opSetFlag(Globals & globals, Strings opFlags, Strings opArgs)
 {
     if (opFlags.size() > 0)
-        throw UsageError(format("unknown flag '%1%'") % opFlags.front());
+        throw UsageError("unknown flag '%1%'", opFlags.front());
     if (opArgs.size() < 2)
         throw UsageError("not enough arguments to '--set-flag'");
 
@@ -680,7 +682,7 @@ static void opSet(Globals & globals, Strings opFlags, Strings opArgs)
     for (Strings::iterator i = opFlags.begin(); i != opFlags.end(); ) {
         string arg = *i++;
         if (parseInstallSourceOptions(globals, i, opFlags, arg)) ;
-        else throw UsageError(format("unknown flag '%1%'") % arg);
+        else throw UsageError("unknown flag '%1%'", arg);
     }
 
     DrvInfos elems;
@@ -759,7 +761,7 @@ static void uninstallDerivations(Globals & globals, Strings & selectors,
 static void opUninstall(Globals & globals, Strings opFlags, Strings opArgs)
 {
     if (opFlags.size() > 0)
-        throw UsageError(format("unknown flag '%1%'") % opFlags.front());
+        throw UsageError("unknown flag '%1%'", opFlags.front());
     uninstallDerivations(globals, opArgs, globals.profile);
 }
 
@@ -872,7 +874,11 @@ static void queryJSON(Globals & globals, vector<DrvInfo> & elems)
             auto placeholder = metaObj.placeholder(j);
             Value * v = i.queryMeta(j);
             if (!v) {
-                printError("derivation '%s' has invalid meta attribute '%s'", i.queryName(), j);
+                logError({ 
+                    .name = "Invalid meta attribute",
+                    .hint = hintfmt("derivation '%s' has invalid meta attribute '%s'",
+                        i.queryName(), j)
+                });
                 placeholder.write(nullptr);
             } else {
                 PathSet context;
@@ -922,7 +928,7 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs)
         else if (arg == "--attr" || arg == "-A")
             attrPath = needArg(i, opFlags, arg);
         else
-            throw UsageError(format("unknown flag '%1%'") % arg);
+            throw UsageError("unknown flag '%1%'", arg);
     }
 
     if (printAttrPath && source != sAvailable)
@@ -1122,8 +1128,13 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs)
                             XMLAttrs attrs2;
                             attrs2["name"] = j;
                             Value * v = i.queryMeta(j);
-                            if (!v)
-                                printError("derivation '%s' has invalid meta attribute '%s'", i.queryName(), j);
+                            if (!v) 
+                                logError({ 
+                                    .name = "Invalid meta attribute",
+                                    .hint = hintfmt(
+                                        "derivation '%s' has invalid meta attribute '%s'",
+                                        i.queryName(), j)
+                                });
                             else {
                                 if (v->type == tString) {
                                     attrs2["type"] = "string";
@@ -1188,9 +1199,9 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs)
 static void opSwitchProfile(Globals & globals, Strings opFlags, Strings opArgs)
 {
     if (opFlags.size() > 0)
-        throw UsageError(format("unknown flag '%1%'") % opFlags.front());
+        throw UsageError("unknown flag '%1%'", opFlags.front());
     if (opArgs.size() != 1)
-        throw UsageError(format("exactly one argument expected"));
+        throw UsageError("exactly one argument expected");
 
     Path profile = absPath(opArgs.front());
     Path profileLink = getHome() + "/.nix-profile";
@@ -1218,10 +1229,10 @@ static void switchGeneration(Globals & globals, int dstGen)
 
     if (!dst) {
         if (dstGen == prevGen)
-            throw Error(format("no generation older than the current (%1%) exists")
-                % curGen);
+            throw Error("no generation older than the current (%1%) exists",
+                curGen);
         else
-            throw Error(format("generation %1% does not exist") % dstGen);
+            throw Error("generation %1% does not exist", dstGen);
     }
 
     printInfo(format("switching from generation %1% to %2%")
@@ -1236,13 +1247,13 @@ static void switchGeneration(Globals & globals, int dstGen)
 static void opSwitchGeneration(Globals & globals, Strings opFlags, Strings opArgs)
 {
     if (opFlags.size() > 0)
-        throw UsageError(format("unknown flag '%1%'") % opFlags.front());
+        throw UsageError("unknown flag '%1%'", opFlags.front());
     if (opArgs.size() != 1)
-        throw UsageError(format("exactly one argument expected"));
+        throw UsageError("exactly one argument expected");
 
     int dstGen;
     if (!string2Int(opArgs.front(), dstGen))
-        throw UsageError(format("expected a generation number"));
+        throw UsageError("expected a generation number");
 
     switchGeneration(globals, dstGen);
 }
@@ -1251,9 +1262,9 @@ static void opSwitchGeneration(Globals & globals, Strings opFlags, Strings opArg
 static void opRollback(Globals & globals, Strings opFlags, Strings opArgs)
 {
     if (opFlags.size() > 0)
-        throw UsageError(format("unknown flag '%1%'") % opFlags.front());
+        throw UsageError("unknown flag '%1%'", opFlags.front());
     if (opArgs.size() != 0)
-        throw UsageError(format("no arguments expected"));
+        throw UsageError("no arguments expected");
 
     switchGeneration(globals, prevGen);
 }
@@ -1262,9 +1273,9 @@ static void opRollback(Globals & globals, Strings opFlags, Strings opArgs)
 static void opListGenerations(Globals & globals, Strings opFlags, Strings opArgs)
 {
     if (opFlags.size() > 0)
-        throw UsageError(format("unknown flag '%1%'") % opFlags.front());
+        throw UsageError("unknown flag '%1%'", opFlags.front());
     if (opArgs.size() != 0)
-        throw UsageError(format("no arguments expected"));
+        throw UsageError("no arguments expected");
 
     PathLocks lock;
     lockProfile(lock, globals.profile);
@@ -1289,7 +1300,7 @@ static void opListGenerations(Globals & globals, Strings opFlags, Strings opArgs
 static void opDeleteGenerations(Globals & globals, Strings opFlags, Strings opArgs)
 {
     if (opFlags.size() > 0)
-        throw UsageError(format("unknown flag '%1%'") % opFlags.front());
+        throw UsageError("unknown flag '%1%'", opFlags.front());
 
     if (opArgs.size() == 1 && opArgs.front() == "old") {
         deleteOldGenerations(globals.profile, globals.dryRun);
@@ -1297,18 +1308,18 @@ static void opDeleteGenerations(Globals & globals, Strings opFlags, Strings opAr
         deleteGenerationsOlderThan(globals.profile, opArgs.front(), globals.dryRun);
     } else if (opArgs.size() == 1 && opArgs.front().find('+') != string::npos) {
         if(opArgs.front().size() < 2)
-            throw Error(format("invalid number of generations ‘%1%’") % opArgs.front());
+            throw Error("invalid number of generations ‘%1%’", opArgs.front());
         string str_max = string(opArgs.front(), 1, opArgs.front().size());
         int max;
         if (!string2Int(str_max, max) || max == 0)
-            throw Error(format("invalid number of generations to keep ‘%1%’") % opArgs.front());
+            throw Error("invalid number of generations to keep ‘%1%’", opArgs.front());
         deleteGenerationsGreaterThan(globals.profile, max, globals.dryRun);
     } else {
         std::set<unsigned int> gens;
         for (auto & i : opArgs) {
             unsigned int n;
             if (!string2Int(i, n))
-                throw UsageError(format("invalid generation number '%1%'") % i);
+                throw UsageError("invalid generation number '%1%'", i);
             gens.insert(n);
         }
         deleteGenerations(globals.profile, gens, globals.dryRun);
diff --git a/src/nix-env/user-env.cc b/src/nix-env/user-env.cc
index f852916d896b3680ea954cc5d70abfac8b58c906..f804b77a04ecfcd91a6ae48a72e9ead6c66f9110 100644
--- a/src/nix-env/user-env.cc
+++ b/src/nix-env/user-env.cc
@@ -146,7 +146,7 @@ bool createUserEnv(EvalState & state, DrvInfos & elems,
 
         Path lockTokenCur = optimisticLockProfile(profile);
         if (lockToken != lockTokenCur) {
-            printError(format("profile '%1%' changed while we were busy; restarting") % profile);
+            printInfo("profile '%1%' changed while we were busy; restarting", profile);
             return false;
         }
 
diff --git a/src/nix-instantiate/nix-instantiate.cc b/src/nix-instantiate/nix-instantiate.cc
index 6c99d11818065b5b423db0572e1cba30f13bf7ca..bf353677a5b93723e01634b10e2381839ab91949 100644
--- a/src/nix-instantiate/nix-instantiate.cc
+++ b/src/nix-instantiate/nix-instantiate.cc
@@ -66,7 +66,7 @@ void processExpr(EvalState & state, const Strings & attrPaths,
                 /* What output do we want? */
                 string outputName = i.queryOutputName();
                 if (outputName == "")
-                    throw Error(format("derivation '%1%' lacks an 'outputName' attribute ") % drvPath);
+                    throw Error("derivation '%1%' lacks an 'outputName' attribute ", drvPath);
 
                 if (gcRoot == "")
                     printGCWarning();
@@ -166,7 +166,7 @@ static int _main(int argc, char * * argv)
         if (findFile) {
             for (auto & i : files) {
                 Path p = state->findFile(i);
-                if (p == "") throw Error(format("unable to find '%1%'") % i);
+                if (p == "") throw Error("unable to find '%1%'", i);
                 std::cout << p << std::endl;
             }
             return 0;
diff --git a/src/nix-prefetch-url/nix-prefetch-url.cc b/src/nix-prefetch-url/nix-prefetch-url.cc
index b645bdc1b0aa254bff543172a80f596352c10658..55b72bda6e67cd15dfb38fb95d11dc61004fc94d 100644
--- a/src/nix-prefetch-url/nix-prefetch-url.cc
+++ b/src/nix-prefetch-url/nix-prefetch-url.cc
@@ -37,11 +37,11 @@ string resolveMirrorUri(EvalState & state, string uri)
 
     auto mirrorList = vMirrors.attrs->find(state.symbols.create(mirrorName));
     if (mirrorList == vMirrors.attrs->end())
-        throw Error(format("unknown mirror name '%1%'") % mirrorName);
+        throw Error("unknown mirror name '%1%'", mirrorName);
     state.forceList(*mirrorList->value);
 
     if (mirrorList->value->listSize() < 1)
-        throw Error(format("mirror URI '%1%' did not expand to anything") % uri);
+        throw Error("mirror URI '%1%' did not expand to anything", uri);
 
     string mirror = state.forceString(*mirrorList->value->listElems()[0]);
     return mirror + (hasSuffix(mirror, "/") ? "" : "/") + string(s, p + 1);
@@ -73,7 +73,7 @@ static int _main(int argc, char * * argv)
                 string s = getArg(*arg, arg, end);
                 ht = parseHashType(s);
                 if (ht == htUnknown)
-                    throw UsageError(format("unknown hash type '%1%'") % s);
+                    throw UsageError("unknown hash type '%1%'", s);
             }
             else if (*arg == "--print-path")
                 printPath = true;
@@ -151,7 +151,7 @@ static int _main(int argc, char * * argv)
         if (name.empty())
             name = baseNameOf(uri);
         if (name.empty())
-            throw Error(format("cannot figure out file name for '%1%'") % uri);
+            throw Error("cannot figure out file name for '%1%'", uri);
 
         /* If an expected hash is given, the file may already exist in
            the store. */
@@ -207,7 +207,7 @@ static int _main(int argc, char * * argv)
             hash = unpack ? hashPath(ht, tmpFile).first : hashFile(ht, tmpFile);
 
             if (expectedHash != Hash(ht) && expectedHash != hash)
-                throw Error(format("hash mismatch for '%1%'") % uri);
+                throw Error("hash mismatch for '%1%'", uri);
 
             const auto recursive = unpack ? FileIngestionMethod::Recursive : FileIngestionMethod::Flat;
 
diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc
index ef8fb0951711780304ec7dfd7ac1eae475aa45af..dea53b52f8f8737669e8ce0e378516f3cd809652 100644
--- a/src/nix-store/nix-store.cc
+++ b/src/nix-store/nix-store.cc
@@ -124,7 +124,7 @@ static void opRealise(Strings opFlags, Strings opArgs)
         else if (i == "--repair") buildMode = bmRepair;
         else if (i == "--check") buildMode = bmCheck;
         else if (i == "--ignore-unknown") ignoreUnknown = true;
-        else throw UsageError(format("unknown flag '%1%'") % i);
+        else throw UsageError("unknown flag '%1%'", i);
 
     std::vector<StorePathWithOutputs> paths;
     for (auto & i : opArgs)
@@ -178,7 +178,7 @@ static void opAddFixed(Strings opFlags, Strings opArgs)
 
     for (auto & i : opFlags)
         if (i == "--recursive") recursive = FileIngestionMethod::Recursive;
-        else throw UsageError(format("unknown flag '%1%'") % i);
+        else throw UsageError("unknown flag '%1%'", i);
 
     if (opArgs.empty())
         throw UsageError("first argument must be hash algorithm");
@@ -198,10 +198,10 @@ static void opPrintFixedPath(Strings opFlags, Strings opArgs)
 
     for (auto i : opFlags)
         if (i == "--recursive") recursive = FileIngestionMethod::Recursive;
-        else throw UsageError(format("unknown flag '%1%'") % i);
+        else throw UsageError("unknown flag '%1%'", i);
 
     if (opArgs.size() != 3)
-        throw UsageError(format("'--print-fixed-path' requires three arguments"));
+        throw UsageError("'--print-fixed-path' requires three arguments");
 
     Strings::iterator i = opArgs.begin();
     HashType hashAlgo = parseHashType(*i++);
@@ -296,9 +296,9 @@ static void opQuery(Strings opFlags, Strings opArgs)
         else if (i == "--use-output" || i == "-u") useOutput = true;
         else if (i == "--force-realise" || i == "--force-realize" || i == "-f") forceRealise = true;
         else if (i == "--include-outputs") includeOutputs = true;
-        else throw UsageError(format("unknown flag '%1%'") % i);
+        else throw UsageError("unknown flag '%1%'", i);
         if (prev != qDefault && prev != query)
-            throw UsageError(format("query type '%1%' conflicts with earlier flag") % i);
+            throw UsageError("query type '%1%' conflicts with earlier flag", i);
     }
 
     if (query == qDefault) query = qOutputs;
@@ -444,7 +444,7 @@ static void opPrintEnv(Strings opFlags, Strings opArgs)
     Derivation drv = store->derivationFromPath(store->parseStorePath(drvPath));
 
     /* Print each environment variable in the derivation in a format
-       that can be sourced by the shell. */
+     * that can be sourced by the shell. */
     for (auto & i : drv.env)
         cout << format("export %1%; %1%=%2%\n") % i.first % shellEscape(i.second);
 
@@ -531,7 +531,7 @@ static void opRegisterValidity(Strings opFlags, Strings opArgs)
     for (auto & i : opFlags)
         if (i == "--reregister") reregister = true;
         else if (i == "--hash-given") hashGiven = true;
-        else throw UsageError(format("unknown flag '%1%'") % i);
+        else throw UsageError("unknown flag '%1%'", i);
 
     if (!opArgs.empty()) throw UsageError("no arguments expected");
 
@@ -545,7 +545,7 @@ static void opCheckValidity(Strings opFlags, Strings opArgs)
 
     for (auto & i : opFlags)
         if (i == "--print-invalid") printInvalid = true;
-        else throw UsageError(format("unknown flag '%1%'") % i);
+        else throw UsageError("unknown flag '%1%'", i);
 
     for (auto & i : opArgs) {
         auto path = store->followLinksToStorePath(i);
@@ -576,7 +576,7 @@ static void opGC(Strings opFlags, Strings opArgs)
             long long maxFreed = getIntArg<long long>(*i, i, opFlags.end(), true);
             options.maxFreed = maxFreed >= 0 ? maxFreed : 0;
         }
-        else throw UsageError(format("bad sub-operation '%1%' in GC") % *i);
+        else throw UsageError("bad sub-operation '%1%' in GC", *i);
 
     if (!opArgs.empty()) throw UsageError("no arguments expected");
 
@@ -612,7 +612,7 @@ static void opDelete(Strings opFlags, Strings opArgs)
 
     for (auto & i : opFlags)
         if (i == "--ignore-liveness") options.ignoreLiveness = true;
-        else throw UsageError(format("unknown flag '%1%'") % i);
+        else throw UsageError("unknown flag '%1%'", i);
 
     for (auto & i : opArgs)
         options.pathsToDelete.insert(store->followLinksToStorePath(i));
@@ -650,7 +650,7 @@ static void opRestore(Strings opFlags, Strings opArgs)
 static void opExport(Strings opFlags, Strings opArgs)
 {
     for (auto & i : opFlags)
-        throw UsageError(format("unknown flag '%1%'") % i);
+        throw UsageError("unknown flag '%1%'", i);
 
     StorePathSet paths;
 
@@ -666,7 +666,7 @@ static void opExport(Strings opFlags, Strings opArgs)
 static void opImport(Strings opFlags, Strings opArgs)
 {
     for (auto & i : opFlags)
-        throw UsageError(format("unknown flag '%1%'") % i);
+        throw UsageError("unknown flag '%1%'", i);
 
     if (!opArgs.empty()) throw UsageError("no arguments expected");
 
@@ -701,10 +701,13 @@ static void opVerify(Strings opFlags, Strings opArgs)
     for (auto & i : opFlags)
         if (i == "--check-contents") checkContents = true;
         else if (i == "--repair") repair = Repair;
-        else throw UsageError(format("unknown flag '%1%'") % i);
+        else throw UsageError("unknown flag '%1%'", i);
 
     if (store->verifyStore(checkContents, repair)) {
-        printError("warning: not all errors were fixed");
+        logWarning({ 
+            .name = "Store consistency",
+            .description = "not all errors were fixed"
+            });
         throw Exit(1);
     }
 }
@@ -726,9 +729,14 @@ static void opVerifyPath(Strings opFlags, Strings opArgs)
         store->narFromPath(path, sink);
         auto current = sink.finish();
         if (current.first != info->narHash) {
-            printError(
-                "path '%s' was modified! expected hash '%s', got '%s'",
-                store->printStorePath(path), info->narHash.to_string(Base32, true), current.first.to_string(Base32, true));
+            logError({ 
+                .name = "Hash mismatch",
+                .hint = hintfmt(
+                    "path '%s' was modified! expected hash '%s', got '%s'",
+                    store->printStorePath(path),
+                    info->narHash.to_string(Base32, true),
+                    current.first.to_string(Base32, true))
+            });
             status = 1;
         }
     }
@@ -764,7 +772,7 @@ static void opServe(Strings opFlags, Strings opArgs)
     bool writeAllowed = false;
     for (auto & i : opFlags)
         if (i == "--write") writeAllowed = true;
-        else throw UsageError(format("unknown flag '%1%'") % i);
+        else throw UsageError("unknown flag '%1%'", i);
 
     if (!opArgs.empty()) throw UsageError("no arguments expected");
 
@@ -835,7 +843,7 @@ static void opServe(Strings opFlags, Strings opArgs)
                             for (auto & p : willSubstitute) subs.emplace_back(p.clone());
                             store->buildPaths(subs);
                         } catch (Error & e) {
-                            printError(format("warning: %1%") % e.msg());
+                            logWarning(e.info());
                         }
                 }
 
@@ -962,7 +970,7 @@ static void opServe(Strings opFlags, Strings opArgs)
             }
 
             default:
-                throw Error(format("unknown serve command %1%") % cmd);
+                throw Error("unknown serve command %1%", cmd);
         }
 
         out.flush();
@@ -973,7 +981,7 @@ static void opServe(Strings opFlags, Strings opArgs)
 static void opGenerateBinaryCacheKey(Strings opFlags, Strings opArgs)
 {
     for (auto & i : opFlags)
-        throw UsageError(format("unknown flag '%1%'") % i);
+        throw UsageError("unknown flag '%1%'", i);
 
     if (opArgs.size() != 3) throw UsageError("three arguments expected");
     auto i = opArgs.begin();
diff --git a/src/nix/cat.cc b/src/nix/cat.cc
index fd91f20362b95f10942c3b8637d53ba6b51149b0..c82819af82414cb290915da52527987ba300eb17 100644
--- a/src/nix/cat.cc
+++ b/src/nix/cat.cc
@@ -13,9 +13,9 @@ struct MixCat : virtual Args
     {
         auto st = accessor->stat(path);
         if (st.type == FSAccessor::Type::tMissing)
-            throw Error(format("path '%1%' does not exist") % path);
+            throw Error("path '%1%' does not exist", path);
         if (st.type != FSAccessor::Type::tRegular)
-            throw Error(format("path '%1%' is not a regular file") % path);
+            throw Error("path '%1%' is not a regular file", path);
 
         std::cout << accessor->readFile(path);
     }
diff --git a/src/nix/hash.cc b/src/nix/hash.cc
index 0a7c343d0dd16a8ea91179e9f5fad6794774c424..0dd4998d99e2e83e5c8e820022034c58b9388679 100644
--- a/src/nix/hash.cc
+++ b/src/nix/hash.cc
@@ -133,7 +133,7 @@ static int compatNixHash(int argc, char * * argv)
             string s = getArg(*arg, arg, end);
             ht = parseHashType(s);
             if (ht == htUnknown)
-                throw UsageError(format("unknown hash type '%1%'") % s);
+                throw UsageError("unknown hash type '%1%'", s);
         }
         else if (*arg == "--to-base16") op = opTo16;
         else if (*arg == "--to-base32") op = opTo32;
diff --git a/src/nix/ls.cc b/src/nix/ls.cc
index b9716a6a106209c16785ce9cc8ee3ec7fa3471ba..d2157f2d4492be4612368ab579e9296b8a83df5c 100644
--- a/src/nix/ls.cc
+++ b/src/nix/ls.cc
@@ -63,7 +63,7 @@ struct MixLs : virtual Args, MixJSON
 
         auto st = accessor->stat(path);
         if (st.type == FSAccessor::Type::tMissing)
-            throw Error(format("path '%1%' does not exist") % path);
+            throw Error("path '%1%' does not exist", path);
         doPath(st, path,
             st.type == FSAccessor::Type::tDirectory ? "." : std::string(baseNameOf(path)),
             showDirectory);
diff --git a/src/nix/make-content-addressable.cc b/src/nix/make-content-addressable.cc
index 3e7ff544d6d3c80a4cad51cb099dca054ad4241b..719ea4fd16d4e4d61ac9264293a8f1f98e234905 100644
--- a/src/nix/make-content-addressable.cc
+++ b/src/nix/make-content-addressable.cc
@@ -85,7 +85,7 @@ struct CmdMakeContentAddressable : StorePathsCommand, MixJSON
             info.ca = makeFixedOutputCA(FileIngestionMethod::Recursive, info.narHash);
 
             if (!json)
-                printError("rewrote '%s' to '%s'", pathS, store->printStorePath(info.path));
+                printInfo("rewrote '%s' to '%s'", pathS, store->printStorePath(info.path));
 
             auto source = sinkToSource([&](Sink & nextSink) {
                 RewritingSink rsink2(oldHashPart, storePathToHash(store->printStorePath(info.path)), nextSink);
diff --git a/src/nix/repl.cc b/src/nix/repl.cc
index ea8ff1553b584897c9a44e3a8b469a99cee7a9d4..4bcaaeebf8d11c35cdf7de37ebca9ee28df53be9 100644
--- a/src/nix/repl.cc
+++ b/src/nix/repl.cc
@@ -218,12 +218,12 @@ void NixRepl::mainLoop(const std::vector<std::string> & files)
                 // input without clearing the input so far.
                 continue;
             } else {
-              printMsg(lvlError, format(error + "%1%%2%") % (settings.showTrace ? e.prefix() : "") % e.msg());
+              printMsg(lvlError, error + "%1%%2%", (settings.showTrace ? e.prefix() : ""), e.msg());
             }
         } catch (Error & e) {
-            printMsg(lvlError, format(error + "%1%%2%") % (settings.showTrace ? e.prefix() : "") % e.msg());
+            printMsg(lvlError, error + "%1%%2%", (settings.showTrace ? e.prefix() : ""), e.msg());
         } catch (Interrupted & e) {
-            printMsg(lvlError, format(error + "%1%%2%") % (settings.showTrace ? e.prefix() : "") % e.msg());
+            printMsg(lvlError, error + "%1%%2%", (settings.showTrace ? e.prefix() : ""), e.msg());
         }
 
         // We handled the current input fully, so we should clear it
@@ -512,7 +512,7 @@ bool NixRepl::processLine(string line)
         return false;
 
     else if (command != "")
-        throw Error(format("unknown command '%1%'") % command);
+        throw Error("unknown command '%1%'", command);
 
     else {
         size_t p = line.find('=');
diff --git a/src/nix/run.cc b/src/nix/run.cc
index b888281a59c3bfe9cd8660c7afb7bdbb94fe49cb..c9b69aec7a6f7f033233b5a8c883774ba34f05fa 100644
--- a/src/nix/run.cc
+++ b/src/nix/run.cc
@@ -197,10 +197,10 @@ void chrootHelper(int argc, char * * argv)
         Finally freeCwd([&]() { free(cwd); });
 
         if (chroot(tmpDir.c_str()) == -1)
-            throw SysError(format("chrooting into '%s'") % tmpDir);
+            throw SysError("chrooting into '%s'", tmpDir);
 
         if (chdir(cwd) == -1)
-            throw SysError(format("chdir to '%s' in chroot") % cwd);
+            throw SysError("chdir to '%s' in chroot", cwd);
     } else
         if (mount(realStoreDir.c_str(), storeDir.c_str(), "", MS_BIND, 0) == -1)
             throw SysError("mounting '%s' on '%s'", realStoreDir, storeDir);
diff --git a/src/nix/upgrade-nix.cc b/src/nix/upgrade-nix.cc
index 678780f333f230373ef406d83a39d6f52a92285f..fdf94e5a37d19b1121b70b69da624c17f0c1b95a 100644
--- a/src/nix/upgrade-nix.cc
+++ b/src/nix/upgrade-nix.cc
@@ -68,7 +68,11 @@ struct CmdUpgradeNix : MixDryRun, StoreCommand
 
         if (dryRun) {
             stopProgressBar();
-            printError("would upgrade to version %s", version);
+            logWarning(
+                ErrorInfo { 
+                    .name = "Version update",
+                    .hint = hintfmt("would upgrade to version %s", version)
+            });
             return;
         }
 
@@ -94,7 +98,7 @@ struct CmdUpgradeNix : MixDryRun, StoreCommand
                 {"--profile", profileDir, "-i", store->printStorePath(storePath), "--no-sandbox"});
         }
 
-        printError(ANSI_GREEN "upgrade to version %s done" ANSI_NORMAL, version);
+        printInfo(ANSI_GREEN "upgrade to version %s done" ANSI_NORMAL, version);
     }
 
     /* Return the profile in which Nix is installed. */
diff --git a/src/nix/verify.cc b/src/nix/verify.cc
index 287dad1012388f4d45e186e3925d6bfe1adac652..246b4b480219001433aa0dc470abe97e6ca35320 100644
--- a/src/nix/verify.cc
+++ b/src/nix/verify.cc
@@ -99,11 +99,15 @@ struct CmdVerify : StorePathsCommand
                     if (hash.first != info->narHash) {
                         corrupted++;
                         act2.result(resCorruptedPath, store->printStorePath(info->path));
-                        printError(
-                            "path '%s' was modified! expected hash '%s', got '%s'",
-                            store->printStorePath(info->path), info->narHash.to_string(Base32, true), hash.first.to_string(Base32, true));
+                        logError({ 
+                            .name = "Hash error - path modified",
+                            .hint = hintfmt(
+                                "path '%s' was modified! expected hash '%s', got '%s'",
+                                store->printStorePath(info->path),
+                                info->narHash.to_string(Base32, true), 
+                                hash.first.to_string(Base32, true))
+                        });
                     }
-
                 }
 
                 if (!noTrust) {
@@ -139,7 +143,7 @@ struct CmdVerify : StorePathsCommand
                                 doSigs(info2->sigs);
                             } catch (InvalidPath &) {
                             } catch (Error & e) {
-                                printError(format(ANSI_RED "error:" ANSI_NORMAL " %s") % e.what());
+                                logError(e.info());
                             }
                         }
 
@@ -150,7 +154,12 @@ struct CmdVerify : StorePathsCommand
                     if (!good) {
                         untrusted++;
                         act2.result(resUntrustedPath, store->printStorePath(info->path));
-                        printError("path '%s' is untrusted", store->printStorePath(info->path));
+                        logError({ 
+                            .name = "Untrusted path",
+                            .hint = hintfmt("path '%s' is untrusted",
+                                store->printStorePath(info->path))
+                        });
+
                     }
 
                 }
@@ -158,7 +167,7 @@ struct CmdVerify : StorePathsCommand
                 done++;
 
             } catch (Error & e) {
-                printError(format(ANSI_RED "error:" ANSI_NORMAL " %s") % e.what());
+                logError(e.info());
                 failed++;
             }
 
diff --git a/src/resolve-system-dependencies/resolve-system-dependencies.cc b/src/resolve-system-dependencies/resolve-system-dependencies.cc
index 8f0c99c84b2827ab8a1268dccdf0f58281723c7b..82feacb3dbb2a5dd7b990dfaf346f096fc79eb3c 100644
--- a/src/resolve-system-dependencies/resolve-system-dependencies.cc
+++ b/src/resolve-system-dependencies/resolve-system-dependencies.cc
@@ -39,12 +39,18 @@ std::set<std::string> runResolver(const Path & filename)
         throw SysError("statting '%s'", filename);
 
     if (!S_ISREG(st.st_mode)) {
-        printError("file '%s' is not a regular file", filename);
+        logError({ 
+            .name = "Regular MACH file",
+            .hint = hintfmt("file '%s' is not a regular file", filename)
+        });
         return {};
     }
 
     if (st.st_size < sizeof(mach_header_64)) {
-        printError("file '%s' is too short for a MACH binary", filename);
+        logError({ 
+            .name = "File too short",
+            .hint = hintfmt("file '%s' is too short for a MACH binary", filename)
+        });
         return {};
     }
 
@@ -66,13 +72,19 @@ std::set<std::string> runResolver(const Path & filename)
             }
         }
         if (mach64_offset == 0) {
-            printError(format("Could not find any mach64 blobs in file '%1%', continuing...") % filename);
+            logError({ 
+                .name = "No mach64 blobs",
+                .hint = hintfmt("Could not find any mach64 blobs in file '%1%', continuing...", filename)
+            });
             return {};
         }
     } else if (magic == MH_MAGIC_64 || magic == MH_CIGAM_64) {
         mach64_offset = 0;
     } else {
-        printError(format("Object file has unknown magic number '%1%', skipping it...") % magic);
+        logError({ 
+            .name = "Magic number",
+            .hint = hintfmt("Object file has unknown magic number '%1%', skipping it...", magic)
+        });
         return {};
     }
 
diff --git a/tests/fetchGitRefs.sh b/tests/fetchGitRefs.sh
index 93993ae9098865757ee9ee8f77fc3dd18834f389..23934698e3ec2c1a9386c10b964b0ec429f059e2 100644
--- a/tests/fetchGitRefs.sh
+++ b/tests/fetchGitRefs.sh
@@ -56,7 +56,7 @@ invalid_ref() {
     else
         (! git check-ref-format --branch "$1" >/dev/null 2>&1)
     fi
-    nix --debug eval --raw "(builtins.fetchGit { url = $repo; ref = ''$1''; }).outPath" 2>&1 | grep 'error: invalid Git branch/tag name' >/dev/null
+    nix --debug eval --raw "(builtins.fetchGit { url = $repo; ref = ''$1''; }).outPath" 2>&1 | grep 'invalid Git branch/tag name' >/dev/null
 }
 
 
diff --git a/tests/misc.sh b/tests/misc.sh
index eda0164167f287a3aa4177e0a19faa36722df5dd..fd4908e25dcd6931ad609756e45edc21cfa4abe1 100644
--- a/tests/misc.sh
+++ b/tests/misc.sh
@@ -16,4 +16,6 @@ nix-env --foo 2>&1 | grep "no operation"
 nix-env -q --foo 2>&1 | grep "unknown flag"
 
 # Eval Errors.
-nix-instantiate --eval -E 'let a = {} // a; in a.foo' 2>&1 | grep "infinite recursion encountered, at .*(string).*:1:15$"
+eval_res=$(nix-instantiate --eval -E 'let a = {} // a; in a.foo' 2>&1 || true)
+echo $eval_res | grep "(string) (1:15)"
+echo $eval_res | grep "infinite recursion encountered"