module V8 # capture 99 stack frames on exception with normal details. # You can adjust these values for performance or turn of stack capture entirely V8::C::V8::SetCaptureStackTraceForUncaughtExceptions(true, 99, V8::C::StackTrace::kOverview) class Error < StandardError include Enumerable # @!attribute [r] value # @return [Object] the JavaScript value passed to the `throw` statement attr_reader :value # @!attribute [r] cause # @return [Exception] the underlying error (if any) that triggered this error to be raised attr_reader :cause # @!attribute [r] javascript_backtrace # @return [V8::StackTrace] the complete JavaScript stack at the point this error was thrown attr_reader :javascript_backtrace # keep an alias to the StandardError#backtrace method so that we can capture # just ruby backtrace frames alias_method :standard_error_backtrace, :backtrace def initialize(message, value, javascript_backtrace, cause = nil) super(message) @value = value @cause = cause @javascript_backtrace = javascript_backtrace end def causes [].tap do |causes| current = self until current.nil? do causes.push current current = current.respond_to?(:cause) ? current.cause : nil end end end def backtrace(*modifiers) return unless super() trace_framework = modifiers.include?(:framework) trace_ruby = modifiers.length == 0 || modifiers.include?(:ruby) trace_javascript = modifiers.length == 0 || modifiers.include?(:javascript) bilingual_backtrace(trace_ruby, trace_javascript).tap do |trace| trace.reject! {|frame| frame =~ %r{(lib/v8/.*\.rb|ext/v8/.*\.cc)}} unless modifiers.include?(:framework) end end def root_cause causes.last end def in_javascript? causes.last.is_a? self.class end def in_ruby? !in_javascript? end def bilingual_backtrace(trace_ruby = true, trace_javascript = true) backtrace = causes.reduce(:backtrace => [], :ruby => -1, :javascript => -1) { |accumulator, cause| accumulator.tap do if trace_ruby backtrace_selector = cause.respond_to?(:standard_error_backtrace) ? :standard_error_backtrace : :backtrace ruby_frames = cause.send(backtrace_selector)[0..accumulator[:ruby]] accumulator[:backtrace].unshift *ruby_frames accumulator[:ruby] -= ruby_frames.length end if trace_javascript && cause.respond_to?(:javascript_backtrace) javascript_frames = cause.javascript_backtrace.to_a[0..accumulator[:javascript]].map(&:to_s) accumulator[:backtrace].unshift *javascript_frames accumulator[:javascript] -= javascript_frames.length end end }[:backtrace] end module Try def try V8::C::TryCatch() do |trycatch| result = yield if trycatch.HasCaught() raise V8::Error(trycatch) else result end end end end module Protect def protect yield rescue Exception => e error = V8::C::Exception::Error(e.message) error.SetHiddenValue("rr::Cause", V8::C::External::New(e)) V8::C::ThrowException(error) end end end # Convert the result of a triggered JavaScript try/catch block into # a V8::Error # # This is a bit of a yak-shave because JavaScript let's you throw all # kinds of things. We do our best to make sure that the message property # of the resulting V8::Error is as helpful as possible, and that it # contains as much source location information as we can put onto it. # # For example: # # throw 4 # throw 'four' # throw {number: 4} # # are all valid cases, none of which actually reference an exception object # with a stack trace and a message. only with something like: # # throw new Error('fail!') # # do you get the a proper stacktrace and a message property. However a lot of # times JavaScript library authors are lazy and do this: # # throw {message: 'foo', otherMetadata: 'bar'} # # It's common enough so we do the courtesy of having the resulting V8::Error # have as its message in ruby land the 'message' property of the value object # # To further complicate things, SyntaxErrors do not have a JavaScript stack # (even if they occur during js execution). This can make debugging a nightmare # so we copy in the source location of the syntax error into the message of # the resulting V8::Error # # @param [V8::C::TryCatch] native trycatch object that has been triggered # @return [V8::Error] the error generated by this try/catch def self.Error(trycatch) exception = trycatch.Exception() value = exception.to_ruby cause = nil message = trycatch.Message() javascript_backtrace = V8::StackTrace.new(message.GetStackTrace()) if message message = if !exception.kind_of?(V8::C::Value) exception.to_s==""?"Script Timed Out":exception.to_s elsif exception.IsNativeError() if cause = exception.GetHiddenValue("rr::Cause") cause = cause.Value() end if value['constructor'] == V8::Context.current['SyntaxError'] info = trycatch.Message() resource_name = info.GetScriptResourceName().to_ruby "#{value['message']} at #{resource_name}:#{info.GetLineNumber()}:#{info.GetStartColumn() + 1}" else exception.Get("message").to_ruby end elsif exception.IsObject() value['message'] || value.to_s else value.to_s end V8::Error.new(message, value, javascript_backtrace, cause) end const_set :JSError, Error end