[ package:vm_service ] 4.0.0 release, Sentinels are now thrown, Future<dynamic> returns are now Future<Response>

Change-Id: Ifd1bf62e3bc33bf14359802086c87b2fd19f3ba9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/138561
Reviewed-by: Jacob Richman <jacobr@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
diff --git a/pkg/vm_service/CHANGELOG.md b/pkg/vm_service/CHANGELOG.md
index 2041a96..6758942 100644
--- a/pkg/vm_service/CHANGELOG.md
+++ b/pkg/vm_service/CHANGELOG.md
@@ -1,7 +1,14 @@
 # Changelog
 
+## 4.0.0
+- **breaking**: RPCs which can return a `Sentinel` will now throw the `Sentinel`
+  it is received as a response.
+- **breaking**: RPCs which can return multiple values now return
+  `Future<Response>` rather than `Future<dynamic>`.
+- `RPCError` now implements `Exception`.
+
 ## 3.0.0
-**breaking**: RPCs which have an isolateId parameter now return
+- **breaking**: RPCs which have an isolateId parameter now return
   `Future<dynamic>` as a `Sentinel` can be returned if the target isolate no
   longer exists.
 
diff --git a/pkg/vm_service/lib/src/vm_service.dart b/pkg/vm_service/lib/src/vm_service.dart
index 0a689b9..7fc8e94 100644
--- a/pkg/vm_service/lib/src/vm_service.dart
+++ b/pkg/vm_service/lib/src/vm_service.dart
@@ -185,47 +185,47 @@
 };
 
 Map<String, List<String>> _methodReturnTypes = {
-  'addBreakpoint': const ['Breakpoint', 'Sentinel'],
-  'addBreakpointWithScriptUri': const ['Breakpoint', 'Sentinel'],
-  'addBreakpointAtEntry': const ['Breakpoint', 'Sentinel'],
-  'clearCpuSamples': const ['Success', 'Sentinel'],
+  'addBreakpoint': const ['Breakpoint'],
+  'addBreakpointWithScriptUri': const ['Breakpoint'],
+  'addBreakpointAtEntry': const ['Breakpoint'],
+  'clearCpuSamples': const ['Success'],
   'clearVMTimeline': const ['Success'],
-  'invoke': const ['InstanceRef', 'ErrorRef', 'Sentinel'],
-  'evaluate': const ['InstanceRef', 'ErrorRef', 'Sentinel'],
-  'evaluateInFrame': const ['InstanceRef', 'ErrorRef', 'Sentinel'],
-  'getAllocationProfile': const ['AllocationProfile', 'Sentinel'],
+  'invoke': const ['InstanceRef', 'ErrorRef'],
+  'evaluate': const ['InstanceRef', 'ErrorRef'],
+  'evaluateInFrame': const ['InstanceRef', 'ErrorRef'],
+  'getAllocationProfile': const ['AllocationProfile'],
   'getClientName': const ['ClientName'],
-  'getCpuSamples': const ['CpuSamples', 'Sentinel'],
+  'getCpuSamples': const ['CpuSamples'],
   'getFlagList': const ['FlagList'],
-  'getInboundReferences': const ['InboundReferences', 'Sentinel'],
-  'getInstances': const ['InstanceSet', 'Sentinel'],
-  'getIsolate': const ['Isolate', 'Sentinel'],
-  'getIsolateGroup': const ['IsolateGroup', 'Sentinel'],
-  'getMemoryUsage': const ['MemoryUsage', 'Sentinel'],
-  'getIsolateGroupMemoryUsage': const ['MemoryUsage', 'Sentinel'],
-  'getScripts': const ['ScriptList', 'Sentinel'],
-  'getObject': const ['Obj', 'Sentinel'],
-  'getRetainingPath': const ['RetainingPath', 'Sentinel'],
-  'getStack': const ['Stack', 'Sentinel'],
-  'getSourceReport': const ['SourceReport', 'Sentinel'],
+  'getInboundReferences': const ['InboundReferences'],
+  'getInstances': const ['InstanceSet'],
+  'getIsolate': const ['Isolate'],
+  'getIsolateGroup': const ['IsolateGroup'],
+  'getMemoryUsage': const ['MemoryUsage'],
+  'getIsolateGroupMemoryUsage': const ['MemoryUsage'],
+  'getScripts': const ['ScriptList'],
+  'getObject': const ['Obj'],
+  'getRetainingPath': const ['RetainingPath'],
+  'getStack': const ['Stack'],
+  'getSourceReport': const ['SourceReport'],
   'getVersion': const ['Version'],
   'getVM': const ['VM'],
   'getVMTimeline': const ['Timeline'],
   'getVMTimelineFlags': const ['TimelineFlags'],
   'getVMTimelineMicros': const ['Timestamp'],
-  'pause': const ['Success', 'Sentinel'],
-  'kill': const ['Success', 'Sentinel'],
+  'pause': const ['Success'],
+  'kill': const ['Success'],
   'registerService': const ['Success'],
-  'reloadSources': const ['ReloadReport', 'Sentinel'],
-  'removeBreakpoint': const ['Success', 'Sentinel'],
-  'requestHeapSnapshot': const ['Success', 'Sentinel'],
+  'reloadSources': const ['ReloadReport'],
+  'removeBreakpoint': const ['Success'],
+  'requestHeapSnapshot': const ['Success'],
   'requirePermissionToResume': const ['Success'],
-  'resume': const ['Success', 'Sentinel'],
+  'resume': const ['Success'],
   'setClientName': const ['Success'],
-  'setExceptionPauseMode': const ['Success', 'Sentinel'],
+  'setExceptionPauseMode': const ['Success'],
   'setFlag': const ['Success', 'Error'],
-  'setLibraryDebuggable': const ['Success', 'Sentinel'],
-  'setName': const ['Success', 'Sentinel'],
+  'setLibraryDebuggable': const ['Success'],
+  'setName': const ['Success'],
   'setVMName': const ['Success'],
   'setVMTimelineFlags': const ['Success'],
   'streamCancel': const ['Success'],
@@ -272,8 +272,9 @@
   ///
   /// See [Breakpoint].
   ///
-  /// The return value can be one of [Breakpoint] or [Sentinel].
-  Future<dynamic> addBreakpoint(
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Breakpoint> addBreakpoint(
     String isolateId,
     String scriptId,
     int line, {
@@ -308,8 +309,9 @@
   ///
   /// See [Breakpoint].
   ///
-  /// The return value can be one of [Breakpoint] or [Sentinel].
-  Future<dynamic> addBreakpointWithScriptUri(
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Breakpoint> addBreakpointWithScriptUri(
     String isolateId,
     String scriptUri,
     int line, {
@@ -329,8 +331,9 @@
   ///
   /// Note that breakpoints are added and removed on a per-isolate basis.
   ///
-  /// The return value can be one of [Breakpoint] or [Sentinel].
-  Future<dynamic> addBreakpointAtEntry(String isolateId, String functionId);
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Breakpoint> addBreakpointAtEntry(String isolateId, String functionId);
 
   /// Clears all CPU profiling samples.
   ///
@@ -339,8 +342,9 @@
   ///
   /// See [Success].
   ///
-  /// The return value can be one of [Success] or [Sentinel].
-  Future<dynamic> clearCpuSamples(String isolateId);
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Success> clearCpuSamples(String isolateId);
 
   /// Clears all VM timeline events.
   ///
@@ -379,8 +383,11 @@
   /// If the invocation is evaluated successfully, an [InstanceRef] reference
   /// will be returned.
   ///
-  /// The return value can be one of [InstanceRef], [ErrorRef] or [Sentinel].
-  Future<dynamic> invoke(
+  /// The return value can be one of [InstanceRef] or [ErrorRef].
+  ///
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Response> invoke(
     String isolateId,
     String targetId,
     String selector,
@@ -422,8 +429,11 @@
   /// If the expression is evaluated successfully, an [InstanceRef] reference
   /// will be returned.
   ///
-  /// The return value can be one of [InstanceRef], [ErrorRef] or [Sentinel].
-  Future<dynamic> evaluate(
+  /// The return value can be one of [InstanceRef] or [ErrorRef].
+  ///
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Response> evaluate(
     String isolateId,
     String targetId,
     String expression, {
@@ -457,8 +467,11 @@
   /// If `isolateId` refers to an isolate which has exited, then the `Collected`
   /// [Sentinel] is returned.
   ///
-  /// The return value can be one of [InstanceRef], [ErrorRef] or [Sentinel].
-  Future<dynamic> evaluateInFrame(
+  /// The return value can be one of [InstanceRef] or [ErrorRef].
+  ///
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Response> evaluateInFrame(
     String isolateId,
     int frameIndex,
     String expression, {
@@ -479,8 +492,10 @@
   /// If `isolateId` refers to an isolate which has exited, then the `Collected`
   /// [Sentinel] is returned.
   ///
-  /// The return value can be one of [AllocationProfile] or [Sentinel].
-  Future<dynamic> getAllocationProfile(String isolateId, {bool reset, bool gc});
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<AllocationProfile> getAllocationProfile(String isolateId,
+      {bool reset, bool gc});
 
   /// The `getClientName` RPC is used to retrieve the name associated with the
   /// currently connected VM service client. If no name was previously set
@@ -500,8 +515,9 @@
   ///
   /// See [CpuSamples].
   ///
-  /// The return value can be one of [CpuSamples] or [Sentinel].
-  Future<dynamic> getCpuSamples(
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<CpuSamples> getCpuSamples(
       String isolateId, int timeOriginMicros, int timeExtentMicros);
 
   /// The `getFlagList` RPC returns a list of all command line flags in the VM
@@ -535,8 +551,9 @@
   ///
   /// See [InboundReferences].
   ///
-  /// The return value can be one of [InboundReferences] or [Sentinel].
-  Future<dynamic> getInboundReferences(
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<InboundReferences> getInboundReferences(
       String isolateId, String targetId, int limit);
 
   /// The `getInstances` RPC is used to retrieve a set of instances which are of
@@ -561,8 +578,10 @@
   ///
   /// See [InstanceSet].
   ///
-  /// The return value can be one of [InstanceSet] or [Sentinel].
-  Future<dynamic> getInstances(String isolateId, String objectId, int limit);
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<InstanceSet> getInstances(
+      String isolateId, String objectId, int limit);
 
   /// The `getIsolate` RPC is used to lookup an `Isolate` object by its `id`.
   ///
@@ -571,8 +590,9 @@
   ///
   /// See [Isolate].
   ///
-  /// The return value can be one of [Isolate] or [Sentinel].
-  Future<dynamic> getIsolate(String isolateId);
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Isolate> getIsolate(String isolateId);
 
   /// The `getIsolateGroup` RPC is used to lookup an `IsolateGroup` object by
   /// its `id`.
@@ -586,8 +606,9 @@
   ///
   /// See [IsolateGroup], [VM].
   ///
-  /// The return value can be one of [IsolateGroup] or [Sentinel].
-  Future<dynamic> getIsolateGroup(String isolateGroupId);
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<IsolateGroup> getIsolateGroup(String isolateGroupId);
 
   /// The `getMemoryUsage` RPC is used to lookup an isolate's memory usage
   /// statistics by its `id`.
@@ -597,8 +618,9 @@
   ///
   /// See [Isolate].
   ///
-  /// The return value can be one of [MemoryUsage] or [Sentinel].
-  Future<dynamic> getMemoryUsage(String isolateId);
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<MemoryUsage> getMemoryUsage(String isolateId);
 
   /// The `getIsolateGroupMemoryUsage` RPC is used to lookup an isolate group's
   /// memory usage statistics by its `id`.
@@ -608,8 +630,9 @@
   ///
   /// See [IsolateGroup].
   ///
-  /// The return value can be one of [MemoryUsage] or [Sentinel].
-  Future<dynamic> getIsolateGroupMemoryUsage(String isolateGroupId);
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<MemoryUsage> getIsolateGroupMemoryUsage(String isolateGroupId);
 
   /// The `getScripts` RPC is used to retrieve a `ScriptList` containing all
   /// scripts for an isolate based on the isolate's `isolateId`.
@@ -619,8 +642,9 @@
   ///
   /// See [ScriptList].
   ///
-  /// The return value can be one of [ScriptList] or [Sentinel].
-  Future<dynamic> getScripts(String isolateId);
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<ScriptList> getScripts(String isolateId);
 
   /// The `getObject` RPC is used to lookup an `object` from some isolate by its
   /// `id`.
@@ -646,8 +670,9 @@
   /// Int32List, Int64List, Flooat32List, Float64List, Inst32x3List,
   /// Float32x4List, and Float64x2List. These parameters are otherwise ignored.
   ///
-  /// The return value can be one of [Obj] or [Sentinel].
-  Future<dynamic> getObject(
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Obj> getObject(
     String isolateId,
     String objectId, {
     int offset,
@@ -676,8 +701,9 @@
   ///
   /// See [RetainingPath].
   ///
-  /// The return value can be one of [RetainingPath] or [Sentinel].
-  Future<dynamic> getRetainingPath(
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<RetainingPath> getRetainingPath(
       String isolateId, String targetId, int limit);
 
   /// The `getStack` RPC is used to retrieve the current execution stack and
@@ -688,8 +714,9 @@
   ///
   /// See [Stack].
   ///
-  /// The return value can be one of [Stack] or [Sentinel].
-  Future<dynamic> getStack(String isolateId);
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Stack> getStack(String isolateId);
 
   /// The `getSourceReport` RPC is used to generate a set of reports tied to
   /// source locations in an isolate.
@@ -728,8 +755,9 @@
   ///
   /// See [SourceReport].
   ///
-  /// The return value can be one of [SourceReport] or [Sentinel].
-  Future<dynamic> getSourceReport(
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<SourceReport> getSourceReport(
     String isolateId,
     /*List<SourceReportKind>*/
     List<String> reports, {
@@ -799,8 +827,9 @@
   ///
   /// See [Success].
   ///
-  /// The return value can be one of [Success] or [Sentinel].
-  Future<dynamic> pause(String isolateId);
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Success> pause(String isolateId);
 
   /// The `kill` RPC is used to kill an isolate as if by dart:isolate's
   /// `Isolate.kill(IMMEDIATE)`.
@@ -812,8 +841,9 @@
   ///
   /// See [Success].
   ///
-  /// The return value can be one of [Success] or [Sentinel].
-  Future<dynamic> kill(String isolateId);
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Success> kill(String isolateId);
 
   /// Registers a service that can be invoked by other VM service clients, where
   /// `service` is the name of the service to advertise and `alias` is an
@@ -843,8 +873,9 @@
   /// If `isolateId` refers to an isolate which has exited, then the `Collected`
   /// [Sentinel] is returned.
   ///
-  /// The return value can be one of [ReloadReport] or [Sentinel].
-  Future<dynamic> reloadSources(
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<ReloadReport> reloadSources(
     String isolateId, {
     bool force,
     bool pause,
@@ -861,8 +892,9 @@
   ///
   /// See [Success].
   ///
-  /// The return value can be one of [Success] or [Sentinel].
-  Future<dynamic> removeBreakpoint(String isolateId, String breakpointId);
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Success> removeBreakpoint(String isolateId, String breakpointId);
 
   /// Requests a dump of the Dart heap of the given isolate.
   ///
@@ -875,8 +907,9 @@
   /// If `isolateId` refers to an isolate which has exited, then the `Collected`
   /// [Sentinel] is returned.
   ///
-  /// The return value can be one of [Success] or [Sentinel].
-  Future<dynamic> requestHeapSnapshot(String isolateId);
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Success> requestHeapSnapshot(String isolateId);
 
   /// The `requirePermissionToResume` RPC is used to change the pause/resume
   /// behavior of isolates by providing a way for the VM service to wait for
@@ -930,8 +963,9 @@
   ///
   /// See [Success], [StepOption].
   ///
-  /// The return value can be one of [Success] or [Sentinel].
-  Future<dynamic> resume(String isolateId,
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Success> resume(String isolateId,
       {/*StepOption*/ String step, int frameIndex});
 
   /// The `setClientName` RPC is used to set a name to be associated with the
@@ -955,8 +989,9 @@
   /// If `isolateId` refers to an isolate which has exited, then the `Collected`
   /// [Sentinel] is returned.
   ///
-  /// The return value can be one of [Success] or [Sentinel].
-  Future<dynamic> setExceptionPauseMode(
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Success> setExceptionPauseMode(
       String isolateId, /*ExceptionPauseMode*/ String mode);
 
   /// The `setFlag` RPC is used to set a VM flag at runtime. Returns an error if
@@ -966,7 +1001,7 @@
   /// The following flags may be set at runtime:
   ///
   /// The return value can be one of [Success] or [Error].
-  Future<dynamic> setFlag(String name, String value);
+  Future<Response> setFlag(String name, String value);
 
   /// The `setLibraryDebuggable` RPC is used to enable or disable whether
   /// breakpoints and stepping work for a given library.
@@ -976,8 +1011,9 @@
   ///
   /// See [Success].
   ///
-  /// The return value can be one of [Success] or [Sentinel].
-  Future<dynamic> setLibraryDebuggable(
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Success> setLibraryDebuggable(
       String isolateId, String libraryId, bool isDebuggable);
 
   /// The `setName` RPC is used to change the debugging name for an isolate.
@@ -987,8 +1023,9 @@
   ///
   /// See [Success].
   ///
-  /// The return value can be one of [Success] or [Sentinel].
-  Future<dynamic> setName(String isolateId, String name);
+  /// This method will throw a [SentinelException] in the case a [Sentinel] is
+  /// returned.
+  Future<Success> setName(String isolateId, String name);
 
   /// The `setVMName` RPC is used to change the debugging name for the vm.
   ///
@@ -1540,7 +1577,7 @@
   Stream<Event> get onStderrEvent => _getEventController('Stderr').stream;
 
   @override
-  Future<dynamic> addBreakpoint(
+  Future<Breakpoint> addBreakpoint(
     String isolateId,
     String scriptId,
     int line, {
@@ -1554,7 +1591,7 @@
       });
 
   @override
-  Future<dynamic> addBreakpointWithScriptUri(
+  Future<Breakpoint> addBreakpointWithScriptUri(
     String isolateId,
     String scriptUri,
     int line, {
@@ -1581,7 +1618,7 @@
   Future<Success> clearVMTimeline() => _call('clearVMTimeline');
 
   @override
-  Future<dynamic> invoke(
+  Future<Response> invoke(
     String isolateId,
     String targetId,
     String selector,
@@ -1598,7 +1635,7 @@
       });
 
   @override
-  Future<dynamic> evaluate(
+  Future<Response> evaluate(
     String isolateId,
     String targetId,
     String expression, {
@@ -1615,7 +1652,7 @@
       });
 
   @override
-  Future<dynamic> evaluateInFrame(
+  Future<Response> evaluateInFrame(
     String isolateId,
     int frameIndex,
     String expression, {
@@ -1656,7 +1693,7 @@
   Future<FlagList> getFlagList() => _call('getFlagList');
 
   @override
-  Future<dynamic> getInboundReferences(
+  Future<InboundReferences> getInboundReferences(
           String isolateId, String targetId, int limit) =>
       _call('getInboundReferences',
           {'isolateId': isolateId, 'targetId': targetId, 'limit': limit});
@@ -1668,19 +1705,19 @@
           {'isolateId': isolateId, 'objectId': objectId, 'limit': limit});
 
   @override
-  Future<dynamic> getIsolate(String isolateId) =>
+  Future<Isolate> getIsolate(String isolateId) =>
       _call('getIsolate', {'isolateId': isolateId});
 
   @override
-  Future<dynamic> getIsolateGroup(String isolateGroupId) =>
+  Future<IsolateGroup> getIsolateGroup(String isolateGroupId) =>
       _call('getIsolateGroup', {'isolateGroupId': isolateGroupId});
 
   @override
-  Future<dynamic> getMemoryUsage(String isolateId) =>
+  Future<MemoryUsage> getMemoryUsage(String isolateId) =>
       _call('getMemoryUsage', {'isolateId': isolateId});
 
   @override
-  Future<dynamic> getIsolateGroupMemoryUsage(String isolateGroupId) =>
+  Future<MemoryUsage> getIsolateGroupMemoryUsage(String isolateGroupId) =>
       _call('getIsolateGroupMemoryUsage', {'isolateGroupId': isolateGroupId});
 
   @override
@@ -1688,7 +1725,7 @@
       _call('getScripts', {'isolateId': isolateId});
 
   @override
-  Future<dynamic> getObject(
+  Future<Obj> getObject(
     String isolateId,
     String objectId, {
     int offset,
@@ -1712,7 +1749,7 @@
       _call('getStack', {'isolateId': isolateId});
 
   @override
-  Future<dynamic> getSourceReport(
+  Future<SourceReport> getSourceReport(
     String isolateId,
     /*List<SourceReportKind>*/
     List<String> reports, {
@@ -1763,7 +1800,7 @@
       _call('registerService', {'service': service, 'alias': alias});
 
   @override
-  Future<dynamic> reloadSources(
+  Future<ReloadReport> reloadSources(
     String isolateId, {
     bool force,
     bool pause,
@@ -1815,7 +1852,7 @@
       _call('setExceptionPauseMode', {'isolateId': isolateId, 'mode': mode});
 
   @override
-  Future<dynamic> setFlag(String name, String value) =>
+  Future<Response> setFlag(String name, String value) =>
       _call('setFlag', {'name': name, 'value': value});
 
   @override
@@ -1984,7 +2021,9 @@
     } else {
       Map<String, dynamic> result = json['result'] as Map<String, dynamic>;
       String type = result['type'];
-      if (_typeFactories[type] == null) {
+      if (type == 'Sentinel') {
+        completer.completeError(SentinelException.parse(methodName, result));
+      } else if (_typeFactories[type] == null) {
         completer.complete(Response.parse(result));
       } else {
         completer.complete(createServiceObject(result, returnTypes));
@@ -2037,7 +2076,7 @@
 
 typedef DisposeHandler = Future Function();
 
-class RPCError {
+class RPCError implements Exception {
   static RPCError parse(String callingMethod, dynamic json) {
     return RPCError(callingMethod, json['code'], json['message'], json['data']);
   }
@@ -2060,6 +2099,17 @@
   }
 }
 
+/// Thrown when an RPC response is a [Sentinel].
+class SentinelException implements Exception {
+  final String callingMethod;
+  final Sentinel sentinel;
+
+  SentinelException.parse(this.callingMethod, Map<String, dynamic> data)
+      : sentinel = Sentinel.parse(data);
+
+  String toString() => '$sentinel from ${callingMethod}()';
+}
+
 /// An `ExtensionData` is an arbitrary map that can have any contents.
 class ExtensionData {
   static ExtensionData parse(Map json) =>
diff --git a/pkg/vm_service/pubspec.yaml b/pkg/vm_service/pubspec.yaml
index 99a3026..366bd74 100644
--- a/pkg/vm_service/pubspec.yaml
+++ b/pkg/vm_service/pubspec.yaml
@@ -2,7 +2,7 @@
 description: >-
   A library to communicate with a service implementing the Dart VM
   service protocol.
-version: 3.0.0+1
+version: 4.0.0
 
 homepage: https://github.com/dart-lang/sdk/tree/master/pkg/vm_service
 
diff --git a/pkg/vm_service/test/async_generator_breakpoint_test.dart b/pkg/vm_service/test/async_generator_breakpoint_test.dart
index 4d52ce4..3cf2655 100644
--- a/pkg/vm_service/test/async_generator_breakpoint_test.dart
+++ b/pkg/vm_service/test/async_generator_breakpoint_test.dart
@@ -48,7 +48,7 @@
 
 Future testAsync(VmService service, IsolateRef isolateRef) async {
   final isolate = await service.getIsolate(isolateRef.id);
-  final lib = await service.getObject(isolate.id, isolate.rootLib.id);
+  final Library lib = await service.getObject(isolate.id, isolate.rootLib.id);
   final script = lib.scripts[0];
 
   final bp1 = await service.addBreakpoint(isolate.id, script.id, 11);
@@ -77,10 +77,10 @@
   // ignore: unawaited_futures
   service
       .evaluate(isolate.id, lib.id, 'testerReady = true')
-      .then((result) async {
-    result = await service.getObject(isolate.id, result.id);
-    print(result);
-    expect((result as Instance).valueAsString, equals('true'));
+      .then((Response result) async {
+    Obj res = await service.getObject(isolate.id, (result as InstanceRef).id);
+    print(res);
+    expect((res as Instance).valueAsString, equals('true'));
   });
 
   final stream = service.onDebugEvent;
diff --git a/pkg/vm_service/test/common/service_test_common.dart b/pkg/vm_service/test/common/service_test_common.dart
index e5dea67..55edf13 100644
--- a/pkg/vm_service/test/common/service_test_common.dart
+++ b/pkg/vm_service/test/common/service_test_common.dart
@@ -117,7 +117,7 @@
   return (VmService service, IsolateRef isolateRef) async {
     print("Setting breakpoint for line $line");
     final isolate = await service.getIsolate(isolateRef.id);
-    final lib = await service.getObject(isolate.id, isolate.rootLib.id);
+    final Library lib = await service.getObject(isolate.id, isolate.rootLib.id);
     final script = lib.scripts.first;
 
     Breakpoint bpt = await service.addBreakpoint(isolate.id, script.id, line);
@@ -139,7 +139,8 @@
     expect(frames.length, greaterThanOrEqualTo(1));
 
     final top = frames[0];
-    final script = await service.getObject(isolate.id, top.location.script.id);
+    final Script script =
+        await service.getObject(isolate.id, top.location.script.id);
     int actualLine = script.getLineNumberFromTokenPos(top.location.tokenPos);
     if (actualLine != line) {
       print("Actual: $actualLine Line: $line");
diff --git a/pkg/vm_service/test/coverage_leaf_function_test.dart b/pkg/vm_service/test/coverage_leaf_function_test.dart
index fe28924..eac959f 100644
--- a/pkg/vm_service/test/coverage_leaf_function_test.dart
+++ b/pkg/vm_service/test/coverage_leaf_function_test.dart
@@ -37,9 +37,11 @@
     expect(stack.frames.length, greaterThanOrEqualTo(1));
     expect(stack.frames[0].function.name, 'testFunction');
 
-    final root = await service.getObject(isolate.id, isolate.rootLib.id);
-    var func = root.functions.singleWhere((f) => f.name == 'leafFunction');
-    func = await service.getObject(isolate.id, func.id);
+    final Library root =
+        await service.getObject(isolate.id, isolate.rootLib.id);
+    FuncRef funcRef =
+        root.functions.singleWhere((f) => f.name == 'leafFunction');
+    Func func = await service.getObject(isolate.id, funcRef.id) as Func;
 
     final expectedRange = {
       'scriptIndex': 0,
@@ -73,9 +75,11 @@
     expect(stack.frames.length, greaterThanOrEqualTo(1));
     expect(stack.frames[0].function.name, 'testFunction');
 
-    final root = await service.getObject(isolate.id, isolate.rootLib.id);
-    var func = root.functions.singleWhere((f) => f.name == 'leafFunction');
-    func = await service.getObject(isolate.id, func.id);
+    final Library root =
+        await service.getObject(isolate.id, isolate.rootLib.id);
+    FuncRef funcRef =
+        root.functions.singleWhere((f) => f.name == 'leafFunction');
+    Func func = await service.getObject(isolate.id, funcRef.id) as Func;
 
     var expectedRange = {
       'scriptIndex': 0,
diff --git a/pkg/vm_service/test/debugging_test.dart b/pkg/vm_service/test/debugging_test.dart
index 8bfefd6..df3a1ed 100644
--- a/pkg/vm_service/test/debugging_test.dart
+++ b/pkg/vm_service/test/debugging_test.dart
@@ -80,16 +80,14 @@
       }
     });
     await service.streamListen(EventStreams.kDebug);
-    final script =
+    final Script script =
         await service.getObject(isolate.id, rootLib.scripts.first.id);
     // Add the breakpoint.
-    final Breakpoint result =
+    final Breakpoint bpt =
         await service.addBreakpoint(isolate.id, script.id, 16);
-    print(result);
-    expect(result is Breakpoint, isTrue);
-    Breakpoint bpt = result;
-    expect(bpt.location.script.id, script.id);
-    expect(script.getLineNumberFromTokenPos(bpt.location.tokenPos), 16);
+    final SourceLocation location = bpt.location;
+    expect(location.script.id, script.id);
+    expect(script.getLineNumberFromTokenPos(location.tokenPos), 16);
 
     isolate = await service.getIsolate(isolate.id);
     expect(isolate.breakpoints.length, 1);
diff --git a/pkg/vm_service/test/eval_test.dart b/pkg/vm_service/test/eval_test.dart
index b751001..4eb8bb5 100644
--- a/pkg/vm_service/test/eval_test.dart
+++ b/pkg/vm_service/test/eval_test.dart
@@ -45,7 +45,7 @@
     // Make sure we are in the right place.
     expect(stack.frames.length, greaterThanOrEqualTo(2));
     expect(stack.frames[0].function.name, 'method');
-    expect(stack.frames[0].function.owner.name, 'MyClass');
+    expect((stack.frames[0].function.owner as ClassRef).name, 'MyClass');
 
     final LibraryRef lib = isolate.rootLib;
     final ClassRef cls = stack.frames[0].function.owner;
@@ -81,11 +81,12 @@
     // Make sure we are in the right place.
     expect(stack.frames.length, greaterThanOrEqualTo(2));
     expect(stack.frames[0].function.name, 'foo');
-    expect(stack.frames[0].function.owner.name, '_MyClass');
+    expect((stack.frames[0].function.owner as ClassRef).name, '_MyClass');
 
     final ClassRef cls = stack.frames[0].function.owner;
 
-    final result = await service.evaluate(isolate.id, cls.id, "1+1");
+    final InstanceRef result =
+        await service.evaluate(isolate.id, cls.id, "1+1");
     print(result);
     expect(result.valueAsString, "2");
   }
diff --git a/pkg/vm_service/test/evaluate_with_scope_test.dart b/pkg/vm_service/test/evaluate_with_scope_test.dart
index ad4a106..38588d8 100644
--- a/pkg/vm_service/test/evaluate_with_scope_test.dart
+++ b/pkg/vm_service/test/evaluate_with_scope_test.dart
@@ -22,13 +22,13 @@
 final tests = <IsolateTest>[
   (VmService service, IsolateRef isolateRef) async {
     final isolate = await service.getIsolate(isolateRef.id);
-    final lib = await service.getObject(isolate.id, isolate.rootLib.id);
+    final Library lib = await service.getObject(isolate.id, isolate.rootLib.id);
 
-    final field1 = await service.getObject(
+    final Field field1 = await service.getObject(
         isolate.id, lib.variables.singleWhere((v) => v.name == 'thing1').id);
     final thing1 = (await service.getObject(isolate.id, field1.staticValue.id));
 
-    final field2 = await service.getObject(
+    final Field field2 = await service.getObject(
         isolate.id, lib.variables.singleWhere((v) => v.name == 'thing2').id);
     final thing2 = (await service.getObject(isolate.id, field2.staticValue.id));
 
diff --git a/pkg/vm_service/test/get_flag_list_rpc_test.dart b/pkg/vm_service/test/get_flag_list_rpc_test.dart
index f7a8b70..8b6c337 100644
--- a/pkg/vm_service/test/get_flag_list_rpc_test.dart
+++ b/pkg/vm_service/test/get_flag_list_rpc_test.dart
@@ -22,16 +22,14 @@
 var tests = <VMTest>[
   // Modify a flag which does not exist.
   (VmService service) async {
-    final result = await service.setFlag('does_not_exist', 'true');
-    expect(result, TypeMatcher<Error>());
+    final Error result = await service.setFlag('does_not_exist', 'true');
     expect(result.message, 'Cannot set flag: flag not found');
   },
 
   // Modify a flag with the wrong value type.
   (VmService service) async {
-    final result =
+    final Error result =
         await service.setFlag('pause_isolates_on_start', 'not-boolean');
-    expect(result, TypeMatcher<Error>());
     expect(result.message, equals('Cannot set flag: invalid value'));
   },
 
@@ -43,8 +41,7 @@
 
   // Modify a flag which cannot be set at runtime.
   (VmService service) async {
-    final result = await service.setFlag('random_seed', '42');
-    expect(result, TypeMatcher<Error>());
+    final Error result = await service.setFlag('random_seed', '42');
     expect(result.message, 'Cannot set flag: cannot change at runtime');
   },
 
diff --git a/pkg/vm_service/test/invoke_test.dart b/pkg/vm_service/test/invoke_test.dart
index dcbb16e..651e39e 100644
--- a/pkg/vm_service/test/invoke_test.dart
+++ b/pkg/vm_service/test/invoke_test.dart
@@ -31,7 +31,7 @@
   hasStoppedAtBreakpoint,
   (VmService service, IsolateRef isolateRef) async {
     final isolate = await service.getIsolate(isolateRef.id);
-    final lib = await service.getObject(isolate.id, isolate.rootLib.id);
+    final Library lib = await service.getObject(isolate.id, isolate.rootLib.id);
     final cls = lib.classes.singleWhere((cls) => cls.name == "Klass");
     FieldRef fieldRef =
         lib.variables.singleWhere((field) => field.name == "instance");
diff --git a/pkg/vm_service/test/throws_sentinel_test.dart b/pkg/vm_service/test/throws_sentinel_test.dart
new file mode 100644
index 0000000..b399f06
--- /dev/null
+++ b/pkg/vm_service/test/throws_sentinel_test.dart
@@ -0,0 +1,23 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:vm_service/vm_service.dart';
+import 'package:test/test.dart';
+
+import 'common/test_helper.dart';
+
+var tests = <VMTest>[
+  (VmService vm) async {
+    try {
+      final res = await vm.getIsolate('isolates/12321');
+      fail('Expected SentinelException, got $res');
+    } on SentinelException catch (e) {
+      // Expected.
+    } catch (e) {
+      fail('Expected SentinelException, got $e');
+    }
+  },
+];
+
+main([args = const <String>[]]) async => await runVMTests(args, tests);
diff --git a/pkg/vm_service/tool/dart/generate_dart.dart b/pkg/vm_service/tool/dart/generate_dart.dart
index 0f62ac2..eb2e6e8 100644
--- a/pkg/vm_service/tool/dart/generate_dart.dart
+++ b/pkg/vm_service/tool/dart/generate_dart.dart
@@ -210,7 +210,9 @@
     } else {
       Map<String, dynamic> result = json['result'] as Map<String, dynamic>;
       String type = result['type'];
-      if (_typeFactories[type] == null) {
+      if (type == 'Sentinel') {
+        completer.completeError(SentinelException.parse(methodName, result));
+      } else if (_typeFactories[type] == null) {
         completer.complete(Response.parse(result));
       } else {
         completer.complete(createServiceObject(result, returnTypes));
@@ -265,7 +267,7 @@
 
 typedef DisposeHandler = Future Function();
 
-class RPCError {
+class RPCError implements Exception {
   static RPCError parse(String callingMethod, dynamic json) {
     return RPCError(callingMethod, json['code'], json['message'], json['data']);
   }
@@ -288,6 +290,17 @@
   }
 }
 
+/// Thrown when an RPC response is a [Sentinel].
+class SentinelException implements Exception {
+  final String callingMethod;
+  final Sentinel sentinel;
+
+  SentinelException.parse(this.callingMethod, Map<String, dynamic> data) :
+    sentinel = Sentinel.parse(data);
+
+  String toString() => '$sentinel from ${callingMethod}()';
+}
+
 /// An `ExtensionData` is an arbitrary map that can have any contents.
 class ExtensionData {
   static ExtensionData parse(Map json) =>
@@ -1110,6 +1123,11 @@
             '${joinLast(returnType.types.map((t) => '[${t}]'), ', ', ' or ')}.';
         _docs = _docs.trim();
       }
+      if (returnType.canReturnSentinel) {
+        _docs +=
+            '\n\nThis method will throw a [SentinelException] in the case a [Sentinel] is returned.';
+        _docs = _docs.trim();
+      }
       if (_docs.isNotEmpty) gen.writeDocs(_docs);
     }
     if (withOverrides) gen.writeln('@override');
@@ -1147,10 +1165,11 @@
 
   MemberType();
 
-  void parse(Parser parser) {
+  void parse(Parser parser, {bool isReturnType = false}) {
     // foo|bar[]|baz
     // (@Instance|Sentinel)[]
     bool loop = true;
+    this.isReturnType = isReturnType;
 
     while (loop) {
       if (parser.consume('(')) {
@@ -1172,7 +1191,11 @@
           parser.expect(']');
           ref.arrayDepth++;
         }
-        types.add(ref);
+        if (isReturnType && ref.name == 'Sentinel') {
+          canReturnSentinel = true;
+        } else {
+          types.add(ref);
+        }
       }
 
       loop = parser.consume('|');
@@ -1182,9 +1205,13 @@
   String get name {
     if (types.isEmpty) return '';
     if (types.length == 1) return types.first.ref;
+    if (isReturnType) return 'Response';
     return 'dynamic';
   }
 
+  bool isReturnType = false;
+  bool canReturnSentinel = false;
+
   bool get isMultipleReturns => types.length > 1;
 
   bool get isSimple => types.length == 1 && types.first.isSimple;
@@ -1923,7 +1950,7 @@
     // method is return type, name, (, args )
     // args is type name, [optional], comma
 
-    method.returnType.parse(this);
+    method.returnType.parse(this, isReturnType: true);
 
     Token t = expectName();
     validate(