diff --git a/base/error.jl b/base/error.jl
index 3282323d0be50..8bb23eba48e38 100644
--- a/base/error.jl
+++ b/base/error.jl
@@ -91,6 +91,30 @@ function catch_backtrace()
     return _reformat_bt(bt[], bt2[])
 end
 
+"""
+    catch_stack(task=current_task(); [inclue_bt=true])
+
+Get the stack of exceptions currently being handled. For nested catch blocks
+there may be more than one current exception in which case the most recently
+thrown exception is last in the stack. The stack is returned as a Vector of
+`(exception,backtrace)` pairs, or a Vector of exceptions if `include_bt` is
+false.
+
+Explicitly passing `task` will return the current exception stack on an
+arbitrary task. This is useful for inspecting tasks which have failed due to
+uncaught exceptions.
+"""
+function catch_stack(task=current_task(); include_bt=true)
+    raw = ccall(:jl_get_exc_stack, Any, (Any,Cint,Cint), task, include_bt, typemax(Cint))
+    formatted = Any[]
+    stride = include_bt ? 3 : 1
+    for i = reverse(1:stride:length(raw))
+        e = raw[i]
+        push!(formatted, include_bt ? (e,Base._reformat_bt(raw[i+1],raw[i+2])) : e)
+    end
+    formatted
+end
+
 ## keyword arg lowering generates calls to this ##
 function kwerr(kw, args::Vararg{Any,N}) where {N}
     @_noinline_meta
diff --git a/base/exports.jl b/base/exports.jl
index e7cdc8808100b..8f7aa256372d8 100644
--- a/base/exports.jl
+++ b/base/exports.jl
@@ -678,6 +678,7 @@ export
 # errors
     backtrace,
     catch_backtrace,
+    catch_stack,
     error,
     rethrow,
     retry,
diff --git a/src/stackwalk.c b/src/stackwalk.c
index 0c288330663a7..09fd03be0b155 100644
--- a/src/stackwalk.c
+++ b/src/stackwalk.c
@@ -177,6 +177,40 @@ JL_DLLEXPORT void jl_get_backtrace(jl_array_t **btout, jl_array_t **bt2out)
     decode_backtrace(bt_data, bt_size, btout, bt2out);
 }
 
+// Return data from the exception stack for `task` as an array of Any, starting
+// with the top of the stack and returning up to `max_entries`. If requested by
+// setting the `include_bt` flag, backtrace data in bt,bt2 format is
+// interleaved.
+JL_DLLEXPORT jl_value_t *jl_get_exc_stack(jl_value_t* task, int include_bt, int max_entries)
+{
+    jl_array_t *stack = NULL;
+    jl_array_t *bt = NULL;
+    jl_array_t *bt2 = NULL;
+    JL_GC_PUSH3(&stack, &bt, &bt2);
+    stack = jl_alloc_array_1d(jl_array_any_type, 0);
+    if (!jl_typeis(task, jl_task_type))
+        jl_error("Cannot get exception stack from a non-Task type");
+    jl_exc_stack_t *s = ((jl_task_t*)task)->exc_stack;
+    if (!s)
+        return (jl_value_t*)stack;
+    size_t itr = s->top;
+    int i = 0;
+    while (itr > 0 && i < max_entries) {
+        jl_array_ptr_1d_push(stack, jl_exc_stack_exception(s, itr));
+        if (include_bt) {
+            decode_backtrace(jl_exc_stack_bt_data(s, itr),
+                             jl_exc_stack_bt_size(s, itr),
+                             &bt, &bt2);
+            jl_array_ptr_1d_push(stack, (jl_value_t*)bt);
+            jl_array_ptr_1d_push(stack, (jl_value_t*)bt2);
+        }
+        itr = jl_exc_stack_next(s, itr);
+        i++;
+    }
+    JL_GC_POP();
+    return (jl_value_t*)stack;
+}
+
 #if defined(_OS_WINDOWS_)
 #ifdef _CPU_X86_64_
 static UNWIND_HISTORY_TABLE HistoryTable;
diff --git a/test/exceptions.jl b/test/exceptions.jl
new file mode 100644
index 0000000000000..24eec56d3e1d4
--- /dev/null
+++ b/test/exceptions.jl
@@ -0,0 +1,210 @@
+using Test
+
+@testset "Exception stack nesting" begin
+    # Basic exception stack handling
+    try
+        error("A")
+    catch
+        @test length(catch_stack()) == 1
+    end
+    @test length(catch_stack()) == 0
+    try
+        try
+            error("A")
+        finally
+            @test length(catch_stack()) == 1
+        end
+    catch
+        @test length(catch_stack()) == 1
+    end
+    # Errors stack up
+    try
+        error("RootCause")
+    catch
+        @test length(catch_stack()) == 1
+        try
+            error("B")
+        catch
+            stack = catch_stack()
+            @test length(stack) == 2
+            @test stack[1][1].msg == "RootCause"
+            @test stack[2][1].msg == "B"
+        end
+        # Stack pops correctly
+        stack = catch_stack()
+        @test length(stack) == 1
+        @test stack[1][1].msg == "RootCause"
+    end
+    # Lowering - value position
+    val = try
+        error("A")
+    catch
+        @test length(catch_stack()) == 1
+        1
+    end
+    @test val == 1
+    function test_exc_stack_tailpos()
+        # exercise lowering code path for tail position
+        try
+            error("A")
+        catch
+            length(catch_stack())
+        end
+    end
+    @test test_exc_stack_tailpos() == 1
+    @test length(catch_stack()) == 0
+end
+
+@testset "Exception stacks and gotos" begin
+    function test_exc_stack_catch_return()
+        try
+            error("A")
+        catch
+            @test length(catch_stack()) == 1
+            return
+        end
+    end
+    test_exc_stack_catch_return()
+    for i=1:1
+        try
+            error("A")
+        catch
+            @test length(catch_stack()) == 1
+            break
+        end
+    end
+    for i=1:1
+        try
+            error("A")
+        catch
+            @test length(catch_stack()) == 1
+            continue
+        end
+    end
+    try
+        error("A")
+    catch
+        @test length(catch_stack()) == 1
+        @goto outofcatch
+    end
+    @label outofcatch
+    @test length(catch_stack()) == 0
+end
+
+@testset "Deep exception stacks" begin
+    function test_exc_stack_deep(n)
+        # Generate deep exception stack with recursive handlers
+        # Note that if you let this overflow the program stack (not the exception
+        # stack) julia will crash. See #28577
+        n != 1 || error("RootCause")
+        try
+            test_exc_stack_deep(n-1)
+        catch
+            error("n==$n")
+        end
+    end
+    @test try
+        test_exc_stack_deep(100)
+    catch
+        length(catch_stack())
+    end == 100
+    @test length(catch_stack()) == 0
+end
+
+@testset "Exception stacks and Tasks" begin
+    # See #12485
+    try
+        error("A")
+    catch
+        t = @task try
+            error("B")
+        catch ex
+            ex
+        end
+        yield(t)
+        @test t.state == :done
+        @test t.result == ErrorException("B")
+        # Task exception state is preserved around task switches
+        @test length(catch_stack()) == 1
+        @test catch_stack()[1][1] == ErrorException("A")
+    end
+    @test length(catch_stack()) == 0
+    # test rethrow() rethrows correct state
+    bt = []
+    try
+        try
+            error("A")
+        catch
+            bt = catch_backtrace()
+            t = @task try
+                error("B")
+            catch ex
+                ex
+            end
+            yield(t)
+            @test t.state == :done
+            @test t.result == ErrorException("B")
+            @test bt == catch_backtrace()
+            rethrow()
+        end
+    catch exc
+        @test exc == ErrorException("A")
+        @test bt == catch_backtrace()
+    end
+    @test length(catch_stack()) == 0
+    # test rethrow with argument
+    bt = []
+    try
+        try
+            error("A")
+        catch
+            t = @task try
+                error("B")
+            catch ex
+                ex
+            end
+            yield(t)
+            @test t.state == :done
+            @test t.result == ErrorException("B")
+            bt = catch_backtrace()
+            rethrow(ErrorException("C"))
+        end
+    catch exc
+        @test exc == ErrorException("C")
+        @test bt == catch_backtrace()
+    end
+    @test length(catch_stack()) == 0
+    # Exception stacks on other tasks
+    t = @task try
+        error("A")
+    catch
+        error("B")
+    end
+    yield(t)
+    @test t.state == :failed
+    @test t.result == ErrorException("B")
+    @test catch_stack(t, include_bt=false) == [ErrorException("A"), ErrorException("B")]
+    # Exception stacks for tasks which never get the chance to start
+    t = @task nothing
+    @test try
+        @async Base.throwto(t, ErrorException("expected"))
+        wait(t)
+    catch e
+        e
+    end == ErrorException("expected")
+    @test length(catch_stack(t)) == 1
+    @test length(catch_stack(t)[1][2]) > 0 # backtrace is nonempty
+end
+
+@testset "rethrow" begin
+    @test try
+        rethrow()
+    catch ex
+        ex
+    end == ErrorException("rethrow() not allowed outside a catch block")
+    @test try
+        rethrow(ErrorException("A"))
+    catch ex
+        ex
+    end == ErrorException("rethrow(exc) not allowed outside a catch block")
+end