diff --git a/LUA_API.rst b/LUA_API.rst
index 0ec837a2d..57a3fa16c 100644
--- a/LUA_API.rst
+++ b/LUA_API.rst
@@ -456,6 +456,31 @@ Currently it defines the following features:
   to group operations together in one big critical section. A plugin
   can choose to run all lua code inside a C++-side suspend lock.
 
+* ``dfhack.call_with_finalizer(num_cleanup_args,always,cleanup_fn[,cleanup_args...],fn[,args...])``
+
+  Invokes ``fn`` with ``args``, and after it returns or throws an
+  error calls ``cleanup_fn`` with ``cleanup_args``. Any return values from
+  ``fn`` are propagated, and errors are re-thrown.
+
+  The ``num_cleanup_args`` integer specifies the number of ``cleanup_args``,
+  and the ``always`` boolean specifies if cleanup should be called in any case,
+  or only in case of an error.
+
+* ``dfhack.with_finalize(cleanup_fn,fn[,args...])``
+
+  Calls ``fn`` with arguments, then finalizes with ``cleanup_fn``.
+  Implemented using ``call_with_finalizer(0,true,...)``.
+
+* ``dfhack.with_onerror(cleanup_fn,fn[,args...])``
+
+  Calls ``fn`` with arguments, then finalizes with ``cleanup_fn`` on any thrown error.
+  Implemented using ``call_with_finalizer(0,false,...)``.
+
+* ``dfhack.with_temp_object(obj,fn[,args...])``
+
+  Calls ``fn(obj,args...)``, then finalizes with ``obj:delete()``.
+
+
 Persistent configuration storage
 ================================
 
diff --git a/Lua API.html b/Lua API.html
index 379432a78..19f912b72 100644
--- a/Lua API.html	
+++ b/Lua API.html	
@@ -724,6 +724,25 @@ the lock. It is safe to nest suspends.
 to group operations together in one big critical section. A plugin
 can choose to run all lua code inside a C++-side suspend lock.
 
+dfhack.call_with_finalizer(num_cleanup_args,always,cleanup_fn[,cleanup_args...],fn[,args...])
+Invokes fn with args, and after it returns or throws an
+error calls cleanup_fn with cleanup_args. Any return values from
+fn are propagated, and errors are re-thrown.
+The num_cleanup_args integer specifies the number of cleanup_args,
+and the always boolean specifies if cleanup should be called in any case,
+or only in case of an error.
+
+dfhack.with_finalize(cleanup_fn,fn[,args...])
+Calls fn with arguments, then finalizes with cleanup_fn.
+Implemented using call_with_finalizer(0,true,...).
+
+dfhack.with_onerror(cleanup_fn,fn[,args...])
+Calls fn with arguments, then finalizes with cleanup_fn on any thrown error.
+Implemented using call_with_finalizer(0,false,...).
+
+dfhack.with_temp_object(obj,fn[,args...])
+Calls fn(obj,args...), then finalizes with obj:delete().
+
 
 
 
@@ -799,7 +818,8 @@ or 
obj.mat_type/
o
 Returns the classification used for craft skills.
 
 info:matches(obj)
-Checks if the material matches job_material_category or job_item.
+Checks if the material matches job_material_category or job_item.
+Accept dfhack_material_category auto-assign table.
 
 
  
diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp
index 1e7a83b30..471ffd550 100644
--- a/library/LuaTools.cpp
+++ b/library/LuaTools.cpp
@@ -226,10 +226,14 @@ static int lua_dfhack_lineedit(lua_State *S)
 
 static int DFHACK_EXCEPTION_META_TOKEN = 0;
 
-static void error_tostring(lua_State *L)
+static void error_tostring(lua_State *L, bool keep_old = false)
 {
     lua_getglobal(L, "tostring");
-    lua_pushvalue(L, -2);
+    if (keep_old)
+        lua_pushvalue(L, -2);
+    else
+        lua_swap(L);
+
     bool ok = lua_pcall(L, 1, 1, 0) == LUA_OK;
 
     const char *msg = lua_tostring(L, -1);
@@ -248,8 +252,7 @@ static void error_tostring(lua_State *L)
 
 static void report_error(lua_State *L, color_ostream *out = NULL)
 {
-    lua_dup(L);
-    error_tostring(L);
+    error_tostring(L, true);
 
     const char *msg = lua_tostring(L, -1);
     assert(msg);
@@ -290,7 +293,7 @@ static bool convert_to_exception(lua_State *L)
             lua_setfield(L, base, "message");
         else
         {
-            error_tostring(L);
+            error_tostring(L, true);
             lua_setfield(L, base, "message");
             lua_setfield(L, base, "object");
         }
@@ -346,24 +349,51 @@ static int dfhack_exception_tostring(lua_State *L)
     if (!lua_isstring(L, -1))
         lua_pop(L, 2);
 
+    lua_pushstring(L, "\ncaused by:\n");
+    lua_getfield(L, 1, "cause");
+    if (lua_isnil(L, -1))
+        lua_pop(L, 2);
+    else
+        error_tostring(L);
+
     lua_concat(L, lua_gettop(L) - base);
     return 1;
 }
 
-static int finish_dfhack_safecall (lua_State *L, bool success)
+static void push_simple_error(lua_State *L, const char *str)
 {
-    if (!lua_checkstack(L, 2))
+    lua_pushstring(L, str);
+
+    if (lua_checkstack(L, 5))
+        convert_to_exception(L);
+
+    if (lua_checkstack(L, LUA_MINSTACK))
+    {
+        luaL_traceback(L, L, NULL, 1);
+        lua_setfield(L, -2, "stacktrace");
+    }
+}
+
+static bool do_finish_pcall(lua_State *L, bool success, int base = 1, int space = 2)
+{
+    if (!lua_checkstack(L, space))
     {
-        lua_settop(L, 0);  /* create space for return values */
+        lua_settop(L, base-1);  /* create space for return values */
         lua_pushboolean(L, 0);
-        lua_pushstring(L, "stack overflow in dfhack.safecall()");
-        success = false;
+        push_simple_error(L, "stack overflow");
+        return false;
     }
     else
     {
         lua_pushboolean(L, success);
-        lua_replace(L, 1); /* put first result in first slot */
+        lua_replace(L, base); /* put first result in first slot */
+        return true;
     }
+}
+
+static int finish_dfhack_safecall (lua_State *L, bool success)
+{
+    success = do_finish_pcall(L, success);
 
     if (!success)
         report_error(L);
@@ -599,32 +629,118 @@ static int lua_dfhack_interpreter(lua_State *state)
     return 1;
 }
 
-static int lua_dfhack_with_suspend(lua_State *L)
+static bool do_invoke_cleanup(lua_State *L, int nargs, int errorfun, bool success)
 {
-    int rv = lua_getctx(L, NULL);
+    bool ok = lua_pcall(L, nargs, 0, errorfun) == LUA_OK;
 
-    // Non-resume entry point:
-    if (rv == LUA_OK)
+    if (!ok)
     {
-        int nargs = lua_gettop(L);
+        // If finalization failed, attach the previous error
+        if (lua_istable(L, -1) && !success)
+        {
+            lua_swap(L);
+            lua_setfield(L, -2, "cause");
+        }
 
-        luaL_checktype(L, 1, LUA_TFUNCTION);
+        success = false;
+    }
+
+    return success;
+}
 
-        Core::getInstance().Suspend();
+static int finish_dfhack_cleanup (lua_State *L, bool success)
+{
+    int nargs = lua_tointeger(L, 1);
+    bool always = lua_toboolean(L, 2);
+    int rvbase = 4+nargs;
+
+    // stack: [nargs] [always] [errorfun] [cleanup fun] [cleanup args...] |rvbase+1:| [rvals/error...]
 
-        lua_pushcfunction(L, dfhack_onerror);
-        lua_insert(L, 1);
+    int numret = lua_gettop(L) - rvbase;
+
+    if (!success || always)
+    {
+        if (numret > 0)
+        {
+            if (numret == 1)
+            {
+                // Inject the only result instead of pulling cleanup args
+                lua_insert(L, 4);
+            }
+            else if (!lua_checkstack(L, nargs+1))
+            {
+                success = false;
+                lua_settop(L, rvbase);
+                push_simple_error(L, "stack overflow");
+                lua_insert(L, 4);
+            }
+            else
+            {
+                for (int i = 0; i <= nargs; i++)
+                    lua_pushvalue(L, 4+i);
+            }
+        }
 
-        rv = lua_pcallk(L, nargs-1, LUA_MULTRET, 1, 0, lua_dfhack_with_suspend);
+        success = do_invoke_cleanup(L, nargs, 3, success);
     }
 
-    // Return, resume, or error entry point:
-    lua_remove(L, 1);
+    if (!success)
+        lua_error(L);
+
+    return numret;
+}
+
+static int dfhack_cleanup_cont (lua_State *L)
+{
+    int status = lua_getctx(L, NULL);
+    return finish_dfhack_cleanup(L, (status == LUA_YIELD));
+}
 
-    Core::getInstance().Resume();
+static int dfhack_call_with_finalizer (lua_State *L)
+{
+    int nargs = luaL_checkint(L, 1);
+    if (nargs < 0)
+        luaL_argerror(L, 1, "invalid cleanup argument count");
+    luaL_checktype(L, 3, LUA_TFUNCTION);
 
-    if (rv != LUA_OK && rv != LUA_YIELD)
+    // Inject errorfun
+    lua_pushcfunction(L, dfhack_onerror);
+    lua_insert(L, 3);
+
+    int rvbase = 4+nargs; // rvbase+1 points to the function argument
+
+    if (lua_gettop(L) < rvbase)
+        luaL_error(L, "not enough arguments even to invoke cleanup");
+
+    // stack: [nargs] [always] [errorfun] [cleanup fun] [cleanup args...] |rvbase+1:| [fun] [args...]
+
+    // Not enough stack to call and post-cleanup, or nothing to call?
+    bool no_args = lua_gettop(L) == rvbase;
+
+    if (!lua_checkstack(L, nargs+2) || no_args)
+    {
+        push_simple_error(L, no_args ? "fn argument expected" : "stack overflow");
+        lua_insert(L, 4);
+
+        // stack: ... [errorfun] [error] [cleanup fun] [cleanup args...]
+        do_invoke_cleanup(L, nargs, 3, false);
         lua_error(L);
+    }
+
+    // Actually invoke
+
+    // stack: [nargs] [always] [errorfun] [cleanup fun] [cleanup args...] |rvbase+1:| [fun] [args...]
+    int status = lua_pcallk(L, lua_gettop(L)-rvbase-1, LUA_MULTRET, 3, 0, dfhack_cleanup_cont);
+    return finish_dfhack_cleanup(L, (status == LUA_OK));
+}
+
+static int lua_dfhack_with_suspend(lua_State *L)
+{
+    int nargs = lua_gettop(L);
+    luaL_checktype(L, 1, LUA_TFUNCTION);
+
+    CoreSuspender suspend;
+    lua_call(L, nargs-1, LUA_MULTRET);
 
     return lua_gettop(L);
 }
@@ -639,6 +755,7 @@ static const luaL_Reg dfhack_funcs[] = {
     { "interpreter", lua_dfhack_interpreter },
     { "safecall", lua_dfhack_safecall },
     { "onerror", dfhack_onerror },
+    { "call_with_finalizer", dfhack_call_with_finalizer },
     { "with_suspend", lua_dfhack_with_suspend },
     { NULL, NULL }
 };
diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua
index d662efed7..b37183cb6 100644
--- a/library/lua/dfhack.lua
+++ b/library/lua/dfhack.lua
@@ -29,6 +29,21 @@ function dfhack.pcall(f, ...)
     return xpcall(f, dfhack.onerror, ...)
 end
 
+function dfhack.with_finalize(...)
+    return dfhack.call_with_finalizer(0,true,...)
+end
+function dfhack.with_onerror(...)
+    return dfhack.call_with_finalizer(0,false,...)
+end
+
+local function call_delete(obj)
+    if obj then obj:delete() end
+end
+
+function dfhack.with_temp_object(obj,fn,...)
+    return dfhack.call_with_finalizer(1,true,call_delete,obj,fn,obj,...)
+end
+
 -- Module loading
 
 function mkmodule(module,env)