diff --git a/src/box/error.cc b/src/box/error.cc
index 8f9bde98775ff00a043a70f2b73e73979686829b..6ccad390efea130d3fd09508ab4f50ae352755bd 100644
--- a/src/box/error.cc
+++ b/src/box/error.cc
@@ -59,8 +59,11 @@ ClientError::log() const
 
 
 uint32_t
-ClientError::get_code_for_foreign_exception(const Exception *e)
+ClientError::get_errcode(const Exception *e)
 {
+	const ClientError *error = dynamic_cast<const ClientError *>(e);
+	if (error)
+		return error->errcode();
 	if (typeid(*e) == typeid(OutOfMemory))
 		return ER_MEMORY_ISSUE;
 	return ER_PROC_LUA;
diff --git a/src/box/error.h b/src/box/error.h
index 908e46c320fa842a02cbb020b74662cfd631943a..caaebc0c6f54ffda39173c14d742c62d44f0e113 100644
--- a/src/box/error.h
+++ b/src/box/error.h
@@ -51,7 +51,7 @@ class ClientError: public Exception {
 	ClientError(const char *file, unsigned line, const char *msg,
 		    uint32_t errcode);
 
-	static uint32_t get_code_for_foreign_exception(const Exception *e);
+	static uint32_t get_errcode(const Exception *e);
 private:
 	/* client errno code */
 	int m_errcode;
diff --git a/src/box/iproto_port.cc b/src/box/iproto_port.cc
index 8cfcb5bfd060460baded7829c46bdb2ab3870590..0624adce98957a840b1eeb37e69842e3393d0edd 100644
--- a/src/box/iproto_port.cc
+++ b/src/box/iproto_port.cc
@@ -79,20 +79,11 @@ iproto_reply_ok(struct obuf *out, uint64_t sync)
 	obuf_dup(out, &empty_map, sizeof(empty_map));
 }
 
-static inline uint32_t
-get_errcode(const Exception *e)
-{
-	const ClientError *error = dynamic_cast<const ClientError *>(e);
-	if (error)
-		return error->errcode();
-	return ClientError::get_code_for_foreign_exception(e);
-}
-
 void
 iproto_reply_error(struct obuf *out, const Exception *e, uint64_t sync)
 {
 	uint32_t msg_len = strlen(e->errmsg());
-	uint32_t errcode = get_errcode(e);
+	uint32_t errcode = ClientError::get_errcode(e);
 
 	struct iproto_header_bin header = iproto_header_bin;
 	struct iproto_body_bin body = iproto_error_bin;
diff --git a/src/box/lua/error.cc b/src/box/lua/error.cc
index e709a6b12bffa9e3295db3d6ea0b212bbd19e9e5..fa7bcdc825097f43b07dec87b9fa63071cd32d44 100644
--- a/src/box/lua/error.cc
+++ b/src/box/lua/error.cc
@@ -41,7 +41,7 @@ extern "C" {
 #include "box/error.h"
 
 static int
-lbox_raise(lua_State *L)
+lbox_error_raise(lua_State *L)
 {
 	uint32_t code = 0;
 	const char *reason = NULL;
@@ -105,6 +105,50 @@ lbox_raise(lua_State *L)
 	return 0;
 }
 
+static int
+lbox_error_last(lua_State *L)
+{
+	if (lua_gettop(L) >= 1)
+		luaL_error(L, "box.error.last(): bad arguments");
+
+	Exception *e = fiber()->exception;
+
+	if (e == NULL) {
+		lua_pushnil(L);
+	} else {
+		lua_newtable(L);
+
+		lua_pushstring(L, "message");
+		lua_pushstring(L, e->errmsg());
+		lua_settable(L, -3);
+
+		lua_pushstring(L, "type");
+		lua_pushstring(L, e->type());
+		lua_settable(L, -3);
+
+		lua_pushstring(L, "code");
+		lua_pushinteger(L, ClientError::get_errcode(e));
+		lua_settable(L, -3);
+
+		if (SystemError *se = dynamic_cast<SystemError *>(e)) {
+			lua_pushstring(L, "errno");
+			lua_pushinteger(L, se->errnum());
+			lua_settable(L, -3);
+		}
+       }
+       return 1;
+}
+
+static int
+lbox_error_clear(lua_State *L)
+{
+	if (lua_gettop(L) >= 1)
+		luaL_error(L, "box.error.clear(): bad arguments");
+
+	Exception::clear(&fiber()->exception);
+	return 0;
+}
+
 static int
 lbox_errinj_set(struct lua_State *L)
 {
@@ -155,9 +199,27 @@ box_lua_error_init(struct lua_State *L) {
 		lua_setfield(L, -2, name + 3);
 	}
 	lua_newtable(L);
-	lua_pushcfunction(L, lbox_raise);
-	lua_setfield(L, -2, "__call");
+	{
+		lua_pushcfunction(L, lbox_error_raise);
+		lua_setfield(L, -2, "__call");
+
+		lua_newtable(L);
+		{
+			lua_pushcfunction(L, lbox_error_last);
+			lua_setfield(L, -2, "last");
+		}
+		{
+			lua_pushcfunction(L, lbox_error_clear);
+			lua_setfield(L, -2, "clear");
+		}
+		{
+			lua_pushcfunction(L, lbox_error_raise);
+			lua_setfield(L, -2, "raise");
+		}
+		lua_setfield(L, -2, "__index");
+	}
 	lua_setmetatable(L, -2);
+
 	lua_pop(L, 1);
 
 	static const struct luaL_reg errinjlib[] = {
diff --git a/src/box/replica.cc b/src/box/replica.cc
index 0d4b72d7f65232d003c1b62afccb6ae287a88c3a..cfd39d88045d5748c617198af9028c08fb93cfda 100644
--- a/src/box/replica.cc
+++ b/src/box/replica.cc
@@ -325,7 +325,7 @@ recovery_stop_remote(struct recovery_state *r)
 	 * If the remote died from an exception, don't throw it
 	 * up.
 	 */
-	Exception::cleanup(&f->exception);
+	Exception::clear(&f->exception);
 	fiber_join(f);
 	r->remote.status = "off";
 }
diff --git a/src/exception.cc b/src/exception.cc
index 851942cd3d48dd26ec14f56e502e21a7902bf1c7..7c7af79865d6729fe1a5e9656945188d2253b8b5 100644
--- a/src/exception.cc
+++ b/src/exception.cc
@@ -84,10 +84,12 @@ Exception::Exception(const Exception& e)
 	memcpy(m_errmsg, e.m_errmsg, sizeof(m_errmsg));
 }
 
-/** A quick & dirty version of name demangle for class names */
-static const char *
-demangle(const char *name)
+
+const char *
+Exception::type() const
 {
+	const char *name = typeid(*this).name();
+	/** A quick & dirty version of name demangle for class names */
 	char *res = NULL;
 	(void) strtol(name, &res, 10);
 	return res && strlen(res) ? res : name;
@@ -96,8 +98,7 @@ demangle(const char *name)
 void
 Exception::log() const
 {
-	_say(S_ERROR, m_file, m_line, m_errmsg, "%s",
-	     demangle(typeid(*this).name()));
+	_say(S_ERROR, m_file, m_line, m_errmsg, "%s", type());
 }
 
 
diff --git a/src/exception.h b/src/exception.h
index 6071eb95bc789940f2af8ec5dc9bf2eea4a7bbe8..c7eec1c14ec925c9ed846cf48b5dc92fabe9524b 100644
--- a/src/exception.h
+++ b/src/exception.h
@@ -47,6 +47,8 @@ class Exception: public Object {
 		throw this;
 	}
 
+	const char *type() const;
+
 	const char *
 	errmsg() const
 	{
@@ -60,8 +62,8 @@ class Exception: public Object {
 	{
 		*what = NULL;
 	}
-	/** Clear the last error saved in the current thread's TLS */
-	static inline void cleanup(Exception **what)
+	/** Clear the last error saved in the current fiber's TLS */
+	static inline void clear(Exception **what)
 	{
 		if (*what != NULL && (*what)->size > 0) {
 			(*what)->~Exception();
@@ -72,7 +74,7 @@ class Exception: public Object {
 	/** Move an exception from one thread to another. */
 	static void move(Exception **from, Exception **to)
 	{
-		Exception::cleanup(to);
+		Exception::clear(to);
 		*to = *from;
 		Exception::init(from);
 	}
diff --git a/src/fiber.cc b/src/fiber.cc
index 111aaca7da0c9b4f106119fa603df2b6810263cd..cbffb62147c9e6bb72b000bd8b75256f47319349 100644
--- a/src/fiber.cc
+++ b/src/fiber.cc
@@ -404,7 +404,7 @@ fiber_loop(void *data __attribute__((unused)))
 			 * Make sure a leftover exception does not
 			 * propagate up to the joiner.
 			 */
-			Exception::cleanup(&fiber->exception);
+			Exception::clear(&fiber->exception);
 		} catch (FiberCancelException *e) {
 			say_info("fiber `%s' has been cancelled",
 				 fiber_name(fiber));
@@ -519,7 +519,7 @@ fiber_destroy(struct cord *cord, struct fiber *f)
 	rlist_del(&f->state);
 	region_destroy(&f->gc);
 	tarantool_coro_destroy(&f->coro, &cord->slabc);
-	Exception::cleanup(&f->exception);
+	Exception::clear(&f->exception);
 }
 
 void
@@ -573,7 +573,7 @@ cord_destroy(struct cord *cord)
 		mh_i32ptr_delete(cord->fiber_registry);
 	}
 	region_destroy(&cord->sched.gc);
-	Exception::cleanup(&cord->sched.exception);
+	Exception::clear(&cord->sched.exception);
 	slab_cache_destroy(&cord->slabc);
 	ev_loop_destroy(cord->loop);
 }
@@ -611,7 +611,7 @@ void *cord_thread_func(void *p)
 		 * Clear a possible leftover exception object
 		 * to not confuse the invoker of the thread.
 		 */
-		Exception::cleanup(&cord->fiber->exception);
+		Exception::clear(&cord->fiber->exception);
 	} catch (Exception *) {
 		/*
 		 * The exception is now available to the caller
diff --git a/test/box/misc.result b/test/box/misc.result
index c95502389faba6cb79e9500ae18f68b8d8ff34c9..a7d37b7b8dd343e34d67274c2e06cfba424e7496 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -41,6 +41,10 @@ t = nil
 ----------------
 --# stop server default
 --# start server default
+box.error.last()
+---
+- null
+...
 box.error({code = 123, reason = 'test'})
 ---
 - error: test
@@ -53,10 +57,26 @@ box.error()
 ---
 - error: Illegal parameters, bla bla
 ...
-box.error()
+box.error.raise()
 ---
 - error: Illegal parameters, bla bla
 ...
+box.error.last()
+---
+- type: ClientError
+  message: Illegal parameters, bla bla
+  code: 1
+...
+box.error.clear()
+---
+...
+box.error.last()
+---
+- null
+...
+box.error.raise()
+---
+...
 space = box.space.tweedledum
 ---
 ...
diff --git a/test/box/misc.test.lua b/test/box/misc.test.lua
index 9b04e65d972d08e39ee9a99ec30a7335884bfd0b..53d2abee6d5140ab5cc8653418dc239f03da2e12 100644
--- a/test/box/misc.test.lua
+++ b/test/box/misc.test.lua
@@ -15,10 +15,15 @@ t = nil
 
 --# stop server default
 --# start server default
+box.error.last()
 box.error({code = 123, reason = 'test'})
 box.error(box.error.ILLEGAL_PARAMS, "bla bla")
 box.error()
-box.error()
+box.error.raise()
+box.error.last()
+box.error.clear()
+box.error.last()
+box.error.raise()
 
 space = box.space.tweedledum
 
diff --git a/test/unit/fiber.cc b/test/unit/fiber.cc
index 451b72bc5f0b98a8a1a06e19e62461934ef50c82..866280a3798482c30fc74867ec5d00a4e22cdbe4 100644
--- a/test/unit/fiber.cc
+++ b/test/unit/fiber.cc
@@ -89,7 +89,7 @@ fiber_join_test()
 	fiber_yield();
 	note("by this time the fiber should be dead already");
 	fiber_cancel(fiber);
-	Exception::cleanup(&fiber->exception);
+	Exception::clear(&fiber->exception);
 	fiber_join(fiber);
 
 	footer();