[pigeon] implements nullable return types (#849)

diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
index 8c7d93a..a8c1255 100644
--- a/packages/pigeon/CHANGELOG.md
+++ b/packages/pigeon/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.0.19
+
+* Implements nullable return types.
+
 ## 1.0.18
 
 * [front-end] Fix error caused by parsing `copyrightHeaders` passed to options in `@ConfigurePigeon`.
diff --git a/packages/pigeon/README.md b/packages/pigeon/README.md
index 9dfcec7..b74f4fe 100644
--- a/packages/pigeon/README.md
+++ b/packages/pigeon/README.md
@@ -64,8 +64,8 @@
    `void`.
 1) Generics are supported, but can currently only be used with nullable types
    (example: `List<int?>`).
-1) Arguments and return values to methods must be non-nullable.  Fields on
-   classes can be nullable or non-nullable.
+1) Arguments must be non-nullable.  Fields on classes and return types can be
+   nullable or non-nullable.
 
 ## Supported Datatypes
 
@@ -124,11 +124,11 @@
 
 1) Nullable method parameters
 1) Nullable generics type arguments
-1) Nullable return values
 
 It does support:
 
 1) Nullable and Non-nullable class fields.
+1) Nullable return values
 
 The default is to generate null-safe code but in order to generate non-null-safe
 code run Pigeon with the extra argument `--no-dart_null_safety`. For example:
diff --git a/packages/pigeon/bin/run_tests.dart b/packages/pigeon/bin/run_tests.dart
index 927c517..a506b78 100644
--- a/packages/pigeon/bin/run_tests.dart
+++ b/packages/pigeon/bin/run_tests.dart
@@ -144,6 +144,8 @@
         '$flutterUnitTestsPath/lib/multiple_arity.gen.dart',
     'pigeons/non_null_fields.dart':
         '$flutterUnitTestsPath/lib/non_null_fields.gen.dart',
+    'pigeons/nullable_returns.dart':
+        '$flutterUnitTestsPath/lib/nullable_returns.gen.dart',
   });
   if (generateCode != 0) {
     return generateCode;
diff --git a/packages/pigeon/lib/dart_generator.dart b/packages/pigeon/lib/dart_generator.dart
index fe40d43..2f735b4 100644
--- a/packages/pigeon/lib/dart_generator.dart
+++ b/packages/pigeon/lib/dart_generator.dart
@@ -186,7 +186,7 @@
         argSignature = _getMethodArgumentsSignature(func, argNameFunc, nullTag);
       }
       indent.write(
-        'Future<${_addGenericTypes(func.returnType, nullTag)}> ${func.name}($argSignature) async ',
+        'Future<${_addGenericTypesNullable(func.returnType, nullTag)}> ${func.name}($argSignature) async ',
       );
       indent.scoped('{', '}', () {
         final String channelName = makeChannelName(api, func);
@@ -200,16 +200,18 @@
         final String returnType =
             _makeGenericTypeArguments(func.returnType, nullTag);
         final String castCall = _makeGenericCastCall(func.returnType, nullTag);
+        const String accessor = 'replyMap[\'${Keys.result}\']';
+        final String unwrapper =
+            func.returnType.isNullable ? '' : unwrapOperator;
         final String returnStatement = func.returnType.isVoid
             ? 'return;'
-            : 'return (replyMap[\'${Keys.result}\'] as $returnType$nullTag)$unwrapOperator$castCall;';
+            : 'return ($accessor as $returnType$nullTag)$unwrapper$castCall;';
         indent.format('''
 final Map<Object$nullTag, Object$nullTag>$nullTag replyMap =\n\t\tawait channel.send($sendArgument) as Map<Object$nullTag, Object$nullTag>$nullTag;
 if (replyMap == null) {
 \tthrow PlatformException(
 \t\tcode: 'channel-error',
 \t\tmessage: 'Unable to establish connection on channel.',
-\t\tdetails: null,
 \t);
 } else if (replyMap['error'] != null) {
 \tfinal Map<Object$nullTag, Object$nullTag> error = (replyMap['${Keys.error}'] as Map<Object$nullTag, Object$nullTag>$nullTag)$unwrapOperator;
@@ -217,7 +219,19 @@
 \t\tcode: (error['${Keys.errorCode}'] as String$nullTag)$unwrapOperator,
 \t\tmessage: error['${Keys.errorMessage}'] as String$nullTag,
 \t\tdetails: error['${Keys.errorDetails}'],
-\t);
+\t);''');
+        // On iOS we can return nil from functions to accommodate error
+        // handling.  Returning a nil value and not returning an error is an
+        // exception.
+        if (!func.returnType.isNullable && !func.returnType.isVoid) {
+          indent.format('''
+} else if (replyMap['${Keys.result}'] == null) {
+\tthrow PlatformException(
+\t\tcode: 'null-error',
+\t\tmessage: 'Host platform returned null value for non-null return value.',
+\t);''');
+        }
+        indent.format('''
 } else {
 \t$returnStatement
 }''');
@@ -255,8 +269,8 @@
     for (final Method func in api.methods) {
       final bool isAsync = func.isAsynchronous;
       final String returnType = isAsync
-          ? 'Future<${_addGenericTypes(func.returnType, nullTag)}>'
-          : _addGenericTypes(func.returnType, nullTag);
+          ? 'Future<${_addGenericTypesNullable(func.returnType, nullTag)}>'
+          : _addGenericTypesNullable(func.returnType, nullTag);
       final String argSignature = _getMethodArgumentsSignature(
         func,
         _getArgumentName,
@@ -294,7 +308,7 @@
             );
             indent.scoped('{', '});', () {
               final String returnType =
-                  _addGenericTypes(func.returnType, nullTag);
+                  _addGenericTypesNullable(func.returnType, nullTag);
               final bool isAsync = func.isAsynchronous;
               final String emptyReturnStatement = isMockHandler
                   ? 'return <Object$nullTag, Object$nullTag>{};'
@@ -387,9 +401,9 @@
   }
 }
 
-String _addGenericTypesNullable(NamedType field, String nullTag) {
-  final String genericdType = _addGenericTypes(field.type, nullTag);
-  return field.type.isNullable ? '$genericdType$nullTag' : genericdType;
+String _addGenericTypesNullable(TypeDeclaration type, String nullTag) {
+  final String genericdType = _addGenericTypes(type, nullTag);
+  return type.isNullable ? '$genericdType$nullTag' : genericdType;
 }
 
 /// Generates Dart source code for the given AST represented by [root],
@@ -495,7 +509,8 @@
             '(pigeonMap[\'${field.name}\'] as $genericType$nullTag)$castCallPrefix$castCall',
           );
         } else {
-          final String genericdType = _addGenericTypesNullable(field, nullTag);
+          final String genericdType =
+              _addGenericTypesNullable(field.type, nullTag);
           if (field.type.isNullable) {
             indent.add(
               'pigeonMap[\'${field.name}\'] as $genericdType',
@@ -532,7 +547,7 @@
       writeConstructor();
       indent.addln('');
       for (final NamedType field in klass.fields) {
-        final String datatype = _addGenericTypesNullable(field, nullTag);
+        final String datatype = _addGenericTypesNullable(field.type, nullTag);
         indent.writeln('$datatype ${field.name};');
       }
       if (klass.fields.isNotEmpty) {
diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart
index 850e6b3..9a7487a 100644
--- a/packages/pigeon/lib/generator_tools.dart
+++ b/packages/pigeon/lib/generator_tools.dart
@@ -8,7 +8,7 @@
 import 'ast.dart';
 
 /// The current version of pigeon. This must match the version in pubspec.yaml.
-const String pigeonVersion = '1.0.18';
+const String pigeonVersion = '1.0.19';
 
 /// Read all the content from [stdin] to a String.
 String readStdin() {
diff --git a/packages/pigeon/lib/java_generator.dart b/packages/pigeon/lib/java_generator.dart
index dd9c391..ef17076 100644
--- a/packages/pigeon/lib/java_generator.dart
+++ b/packages/pigeon/lib/java_generator.dart
@@ -118,33 +118,134 @@
 void _writeHostApi(Indent indent, Api api) {
   assert(api.location == ApiLocation.host);
 
+  /// Write a method in the interface.
+  /// Example:
+  ///   int add(int x, int y);
+  void writeInterfaceMethod(final Method method) {
+    final String returnType = method.isAsynchronous
+        ? 'void'
+        : _nullsafeJavaTypeForDartType(method.returnType);
+    final List<String> argSignature = <String>[];
+    if (method.arguments.isNotEmpty) {
+      final Iterable<String> argTypes =
+          method.arguments.map((NamedType e) => _javaTypeForDartType(e.type));
+      final Iterable<String> argNames =
+          method.arguments.map((NamedType e) => e.name);
+      argSignature
+          .addAll(map2(argTypes, argNames, (String argType, String argName) {
+        return '$argType $argName';
+      }));
+    }
+    if (method.isAsynchronous) {
+      final String resultType = method.returnType.isVoid
+          ? 'Void'
+          : _javaTypeForDartType(method.returnType);
+      argSignature.add('Result<$resultType> result');
+    }
+    indent.writeln('$returnType ${method.name}(${argSignature.join(', ')});');
+  }
+
+  /// Write a static setup function in the interface.
+  /// Example:
+  ///   static void setup(BinaryMessenger binaryMessenger, Foo api) {...}
+  void writeMethodSetup(final Method method) {
+    final String channelName = makeChannelName(api, method);
+    indent.write('');
+    indent.scoped('{', '}', () {
+      indent.writeln('BasicMessageChannel<Object> channel =');
+      indent.inc();
+      indent.inc();
+      indent.writeln(
+          'new BasicMessageChannel<>(binaryMessenger, "$channelName", getCodec());');
+      indent.dec();
+      indent.dec();
+      indent.write('if (api != null) ');
+      indent.scoped('{', '} else {', () {
+        indent.write('channel.setMessageHandler((message, reply) -> ');
+        indent.scoped('{', '});', () {
+          final String returnType = method.returnType.isVoid
+              ? 'Void'
+              : _javaTypeForDartType(method.returnType);
+          indent.writeln('Map<String, Object> wrapped = new HashMap<>();');
+          indent.write('try ');
+          indent.scoped('{', '}', () {
+            final List<String> methodArgument = <String>[];
+            if (method.arguments.isNotEmpty) {
+              indent.writeln(
+                  'ArrayList<Object> args = (ArrayList<Object>)message;');
+              enumerate(method.arguments, (int index, NamedType arg) {
+                // The StandardMessageCodec can give us [Integer, Long] for
+                // a Dart 'int'.  To keep things simple we just use 64bit
+                // longs in Pigeon with Java.
+                final bool isInt = arg.type.baseName == 'int';
+                final String argType =
+                    isInt ? 'Number' : _javaTypeForDartType(arg.type);
+                final String argCast = isInt ? '.longValue()' : '';
+                final String argName = _getSafeArgumentName(index, arg);
+                indent
+                    .writeln('$argType $argName = ($argType)args.get($index);');
+                indent.write('if ($argName == null) ');
+                indent.scoped('{', '}', () {
+                  indent.writeln(
+                      'throw new NullPointerException("$argName unexpectedly null.");');
+                });
+                methodArgument.add('$argName$argCast');
+              });
+            }
+            if (method.isAsynchronous) {
+              final String resultValue =
+                  method.returnType.isVoid ? 'null' : 'result';
+              const String resultName = 'resultCallback';
+              indent.format('''
+Result<$returnType> $resultName = new Result<$returnType>() {
+\tpublic void success($returnType result) {
+\t\twrapped.put("${Keys.result}", $resultValue);
+\t\treply.reply(wrapped);
+\t}
+\tpublic void error(Throwable error) {
+\t\twrapped.put("${Keys.error}", wrapError(error));
+\t\treply.reply(wrapped);
+\t}
+};
+''');
+              methodArgument.add(resultName);
+            }
+            final String call =
+                'api.${method.name}(${methodArgument.join(', ')})';
+            if (method.isAsynchronous) {
+              indent.writeln('$call;');
+            } else if (method.returnType.isVoid) {
+              indent.writeln('$call;');
+              indent.writeln('wrapped.put("${Keys.result}", null);');
+            } else {
+              indent.writeln('$returnType output = $call;');
+              indent.writeln('wrapped.put("${Keys.result}", output);');
+            }
+          });
+          indent.write('catch (Error | RuntimeException exception) ');
+          indent.scoped('{', '}', () {
+            indent
+                .writeln('wrapped.put("${Keys.error}", wrapError(exception));');
+            if (method.isAsynchronous) {
+              indent.writeln('reply.reply(wrapped);');
+            }
+          });
+          if (!method.isAsynchronous) {
+            indent.writeln('reply.reply(wrapped);');
+          }
+        });
+      });
+      indent.scoped(null, '}', () {
+        indent.writeln('channel.setMessageHandler(null);');
+      });
+    });
+  }
+
   indent.writeln(
       '/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/');
   indent.write('public interface ${api.name} ');
   indent.scoped('{', '}', () {
-    for (final Method method in api.methods) {
-      final String returnType = method.isAsynchronous
-          ? 'void'
-          : _javaTypeForDartType(method.returnType);
-      final List<String> argSignature = <String>[];
-      if (method.arguments.isNotEmpty) {
-        final Iterable<String> argTypes =
-            method.arguments.map((NamedType e) => _javaTypeForDartType(e.type));
-        final Iterable<String> argNames =
-            method.arguments.map((NamedType e) => e.name);
-        argSignature
-            .addAll(map2(argTypes, argNames, (String argType, String argName) {
-          return '$argType $argName';
-        }));
-      }
-      if (method.isAsynchronous) {
-        final String returnType = method.returnType.isVoid
-            ? 'Void'
-            : _javaTypeForDartType(method.returnType);
-        argSignature.add('Result<$returnType> result');
-      }
-      indent.writeln('$returnType ${method.name}(${argSignature.join(', ')});');
-    }
+    api.methods.forEach(writeInterfaceMethod);
     indent.addln('');
     final String codecName = _getCodecName(api);
     indent.format('''
@@ -158,98 +259,7 @@
     indent.write(
         'static void setup(BinaryMessenger binaryMessenger, ${api.name} api) ');
     indent.scoped('{', '}', () {
-      for (final Method method in api.methods) {
-        final String channelName = makeChannelName(api, method);
-        indent.write('');
-        indent.scoped('{', '}', () {
-          indent.writeln('BasicMessageChannel<Object> channel =');
-          indent.inc();
-          indent.inc();
-          indent.writeln(
-              'new BasicMessageChannel<>(binaryMessenger, "$channelName", getCodec());');
-          indent.dec();
-          indent.dec();
-          indent.write('if (api != null) ');
-          indent.scoped('{', '} else {', () {
-            indent.write('channel.setMessageHandler((message, reply) -> ');
-            indent.scoped('{', '});', () {
-              final String returnType = method.returnType.isVoid
-                  ? 'Void'
-                  : _javaTypeForDartType(method.returnType);
-              indent.writeln('Map<String, Object> wrapped = new HashMap<>();');
-              indent.write('try ');
-              indent.scoped('{', '}', () {
-                final List<String> methodArgument = <String>[];
-                if (method.arguments.isNotEmpty) {
-                  indent.writeln(
-                      'ArrayList<Object> args = (ArrayList<Object>)message;');
-                  enumerate(method.arguments, (int index, NamedType arg) {
-                    // The StandardMessageCodec can give us [Integer, Long] for
-                    // a Dart 'int'.  To keep things simple we just use 64bit
-                    // longs in Pigeon with Java.
-                    final bool isInt = arg.type.baseName == 'int';
-                    final String argType =
-                        isInt ? 'Number' : _javaTypeForDartType(arg.type);
-                    final String argCast = isInt ? '.longValue()' : '';
-                    final String argName = _getSafeArgumentName(index, arg);
-                    indent.writeln(
-                        '$argType $argName = ($argType)args.get($index);');
-                    indent.write('if ($argName == null) ');
-                    indent.scoped('{', '}', () {
-                      indent.writeln(
-                          'throw new NullPointerException("$argName unexpectedly null.");');
-                    });
-                    methodArgument.add('$argName$argCast');
-                  });
-                }
-                if (method.isAsynchronous) {
-                  final String resultValue =
-                      method.returnType.isVoid ? 'null' : 'result';
-                  const String resultName = 'resultCallback';
-                  indent.format('''
-Result<$returnType> $resultName = new Result<$returnType>() {
-\tpublic void success($returnType result) {
-\t\twrapped.put("${Keys.result}", $resultValue);
-\t\treply.reply(wrapped);
-\t}
-\tpublic void error(Throwable error) {
-\t\twrapped.put("${Keys.error}", wrapError(error));
-\t\treply.reply(wrapped);
-\t}
-};
-''');
-                  methodArgument.add(resultName);
-                }
-                final String call =
-                    'api.${method.name}(${methodArgument.join(', ')})';
-                if (method.isAsynchronous) {
-                  indent.writeln('$call;');
-                } else if (method.returnType.isVoid) {
-                  indent.writeln('$call;');
-                  indent.writeln('wrapped.put("${Keys.result}", null);');
-                } else {
-                  indent.writeln('$returnType output = $call;');
-                  indent.writeln('wrapped.put("${Keys.result}", output);');
-                }
-              });
-              indent.write('catch (Error | RuntimeException exception) ');
-              indent.scoped('{', '}', () {
-                indent.writeln(
-                    'wrapped.put("${Keys.error}", wrapError(exception));');
-                if (method.isAsynchronous) {
-                  indent.writeln('reply.reply(wrapped);');
-                }
-              });
-              if (!method.isAsynchronous) {
-                indent.writeln('reply.reply(wrapped);');
-              }
-            });
-          });
-          indent.scoped(null, '}', () {
-            indent.writeln('channel.setMessageHandler(null);');
-          });
-        });
-      }
+      api.methods.forEach(writeMethodSetup);
     });
   });
 }
@@ -393,6 +403,11 @@
   return _javaTypeForBuiltinDartType(type) ?? type.baseName;
 }
 
+String _nullsafeJavaTypeForDartType(TypeDeclaration type) {
+  final String nullSafe = type.isNullable ? '@Nullable' : '@NonNull';
+  return '$nullSafe ${_javaTypeForDartType(type)}';
+}
+
 /// Casts variable named [varName] to the correct host datatype for [field].
 /// This is for use in codecs where we may have a map representation of an
 /// object.
diff --git a/packages/pigeon/lib/objc_generator.dart b/packages/pigeon/lib/objc_generator.dart
index 8ef3986..87a8d23 100644
--- a/packages/pigeon/lib/objc_generator.dart
+++ b/packages/pigeon/lib/objc_generator.dart
@@ -69,7 +69,7 @@
 String _callbackForType(TypeDeclaration type, _ObjcPtr objcType) {
   return type.isVoid
       ? 'void(^)(NSError *_Nullable)'
-      : 'void(^)(${objcType.ptr.trim()}, NSError *_Nullable)';
+      : 'void(^)(${objcType.ptr.trim()}_Nullable, NSError *_Nullable)';
 }
 
 /// Represents an ObjC pointer (ex 'id', 'NSString *').
@@ -427,6 +427,9 @@
       lastArgType = 'FlutterError *_Nullable *_Nonnull';
       lastArgName = 'error';
     }
+    if (!func.returnType.isNullable) {
+      indent.writeln('/// @return `nil` only when `error != nil`.');
+    }
     indent.writeln(_makeObjcSignature(
             func: func,
             options: options,
diff --git a/packages/pigeon/lib/pigeon_lib.dart b/packages/pigeon/lib/pigeon_lib.dart
index 428056c..71082dd 100644
--- a/packages/pigeon/lib/pigeon_lib.dart
+++ b/packages/pigeon/lib/pigeon_lib.dart
@@ -474,13 +474,6 @@
   }
   for (final Api api in root.apis) {
     for (final Method method in api.methods) {
-      if (method.returnType.isNullable) {
-        result.add(Error(
-          message:
-              'Nullable return types types aren\'t supported for Pigeon methods: "${method.returnType.baseName}" in API: "${api.name}" method: "${method.name}"',
-          lineNumber: _calculateLineNumberNullable(source, method.offset),
-        ));
-      }
       if (method.arguments.isNotEmpty &&
           method.arguments.any((NamedType element) =>
               customEnums.contains(element.type.baseName))) {
diff --git a/packages/pigeon/pigeons/nullable_returns.dart b/packages/pigeon/pigeons/nullable_returns.dart
new file mode 100644
index 0000000..caa33ef
--- /dev/null
+++ b/packages/pigeon/pigeons/nullable_returns.dart
@@ -0,0 +1,18 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This file is an example pigeon file that is used in compilation, unit, mock
+// handler, and e2e tests.
+
+import 'package:pigeon/pigeon.dart';
+
+@HostApi()
+abstract class NonNullHostApi {
+  int? doit();
+}
+
+@FlutterApi()
+abstract class NonNullFlutterApi {
+  int? doit();
+}
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/all_datatypes.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/all_datatypes.dart
index d71bddc..f318dc5 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/all_datatypes.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/all_datatypes.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v1.0.15), do not edit directly.
+// Autogenerated from Pigeon (v1.0.19), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
 // @dart = 2.12
@@ -130,7 +130,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -140,6 +139,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as Everything?)!;
     }
@@ -155,7 +159,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -165,6 +168,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as Everything?)!;
     }
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/multiple_arity.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/multiple_arity.gen.dart
index 6421b9f..8c80603 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/multiple_arity.gen.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/multiple_arity.gen.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v1.0.15), do not edit directly.
+// Autogenerated from Pigeon (v1.0.19), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
 // @dart = 2.12
@@ -37,7 +37,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -47,6 +46,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as int?)!;
     }
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart
index c894284..97ea6a4 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v1.0.15), do not edit directly.
+// Autogenerated from Pigeon (v1.0.19), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
 // @dart = 2.12
@@ -28,7 +28,7 @@
   static SearchRequest decode(Object message) {
     final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
     return SearchRequest(
-      query: (pigeonMap['query'] as String?)!,
+      query: pigeonMap['query']! as String,
     );
   }
 }
@@ -55,8 +55,8 @@
   static SearchReply decode(Object message) {
     final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
     return SearchReply(
-      result: (pigeonMap['result'] as String?)!,
-      error: (pigeonMap['error'] as String?)!,
+      result: pigeonMap['result']! as String,
+      error: pigeonMap['error']! as String,
       indices: (pigeonMap['indices'] as List<Object?>?)!.cast<int?>(),
     );
   }
@@ -113,7 +113,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -123,6 +122,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as SearchReply?)!;
     }
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart
index 5b3c5c0..5cce608 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v1.0.15), do not edit directly.
+// Autogenerated from Pigeon (v1.0.19), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
 // @dart = 2.12
@@ -162,7 +162,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -172,6 +171,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as SearchReply?)!;
     }
@@ -187,7 +191,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -197,6 +200,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as SearchReplies?)!;
     }
@@ -212,7 +220,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -222,6 +229,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as SearchRequests?)!;
     }
@@ -237,7 +249,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -247,6 +258,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as int?)!;
     }
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart
new file mode 100644
index 0000000..f80c7d3
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart
@@ -0,0 +1,80 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Autogenerated from Pigeon (v1.0.19), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
+// @dart = 2.12
+import 'dart:async';
+import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List;
+
+import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer;
+import 'package:flutter/services.dart';
+
+class _NonNullHostApiCodec extends StandardMessageCodec {
+  const _NonNullHostApiCodec();
+}
+
+class NonNullHostApi {
+  /// Constructor for [NonNullHostApi].  The [binaryMessenger] named argument is
+  /// available for dependency injection.  If it is left null, the default
+  /// BinaryMessenger will be used which routes to the host platform.
+  NonNullHostApi({BinaryMessenger? binaryMessenger})
+      : _binaryMessenger = binaryMessenger;
+
+  final BinaryMessenger? _binaryMessenger;
+
+  static const MessageCodec<Object?> codec = _NonNullHostApiCodec();
+
+  Future<int?> doit() async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.NonNullHostApi.doit', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(null) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return (replyMap['result'] as int?);
+    }
+  }
+}
+
+class _NonNullFlutterApiCodec extends StandardMessageCodec {
+  const _NonNullFlutterApiCodec();
+}
+
+abstract class NonNullFlutterApi {
+  static const MessageCodec<Object?> codec = _NonNullFlutterApiCodec();
+
+  int? doit();
+  static void setup(NonNullFlutterApi? api,
+      {BinaryMessenger? binaryMessenger}) {
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.NonNullFlutterApi.doit', codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMessageHandler(null);
+      } else {
+        channel.setMessageHandler((Object? message) async {
+          // ignore message
+          final int? output = api.doit();
+          return output;
+        });
+      }
+    }
+  }
+}
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.dart
index 780b412..b71adc3 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 //
-// Autogenerated from Pigeon (v1.0.15), do not edit directly.
+// Autogenerated from Pigeon (v1.0.19), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
 // @dart = 2.12
@@ -37,7 +37,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -47,6 +46,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as int?)!;
     }
@@ -62,7 +66,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -72,6 +75,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as bool?)!;
     }
@@ -87,7 +95,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -97,6 +104,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as String?)!;
     }
@@ -112,7 +124,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -122,6 +133,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as double?)!;
     }
@@ -137,7 +153,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -147,6 +162,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as Map<Object?, Object?>?)!;
     }
@@ -162,7 +182,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -172,6 +191,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as List<Object?>?)!;
     }
@@ -187,7 +211,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -197,6 +220,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as Int32List?)!;
     }
@@ -212,7 +240,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -222,6 +249,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as List<Object?>?)!.cast<bool?>();
     }
@@ -237,7 +269,6 @@
       throw PlatformException(
         code: 'channel-error',
         message: 'Unable to establish connection on channel.',
-        details: null,
       );
     } else if (replyMap['error'] != null) {
       final Map<Object?, Object?> error =
@@ -247,6 +278,11 @@
         message: error['message'] as String?,
         details: error['details'],
       );
+    } else if (replyMap['result'] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
     } else {
       return (replyMap['result'] as Map<Object?, Object?>?)!
           .cast<String?, int?>();
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart
index b00362c..e412b78 100644
--- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart
@@ -77,4 +77,16 @@
     final int result = await api.anInt(1);
     expect(result, 1);
   });
+
+  test('return null to nonnull', () async {
+    final BinaryMessenger mockMessenger = MockBinaryMessenger();
+    const String channel = 'dev.flutter.pigeon.Api.anInt';
+    when(mockMessenger.send(channel, any))
+        .thenAnswer((Invocation realInvocation) async {
+      return Api.codec.encodeMessage(<String?, Object?>{'result': null});
+    });
+    final Api api = Api(binaryMessenger: mockMessenger);
+    expect(() async => api.anInt(1),
+        throwsA(const TypeMatcher<PlatformException>()));
+  });
 }
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj
index 0a81cb2..ed58c5c 100644
--- a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj
@@ -3,10 +3,11 @@
 	archiveVersion = 1;
 	classes = {
 	};
-	objectVersion = 50;
+	objectVersion = 46;
 	objects = {
 
 /* Begin PBXBuildFile section */
+		0D02163D27BC7B48009BD76F /* nullable_returns.gen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D02163C27BC7B48009BD76F /* nullable_returns.gen.m */; };
 		0D50127523FF75B100CD5B95 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D50127423FF75B100CD5B95 /* RunnerTests.m */; };
 		0D6FD3C526A76D400046D8BD /* primitive.gen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6FD3C426A76D400046D8BD /* primitive.gen.m */; };
 		0D6FD3C726A777C00046D8BD /* PrimitiveTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6FD3C626A777C00046D8BD /* PrimitiveTest.m */; };
@@ -64,6 +65,8 @@
 /* End PBXCopyFilesBuildPhase section */
 
 /* Begin PBXFileReference section */
+		0D02163B27BC7B48009BD76F /* nullable_returns.gen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = nullable_returns.gen.h; sourceTree = "<group>"; };
+		0D02163C27BC7B48009BD76F /* nullable_returns.gen.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = nullable_returns.gen.m; sourceTree = "<group>"; };
 		0D50127223FF75B100CD5B95 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		0D50127423FF75B100CD5B95 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = "<group>"; };
 		0D50127623FF75B100CD5B95 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -193,6 +196,8 @@
 		97C146F01CF9000F007C117D /* Runner */ = {
 			isa = PBXGroup;
 			children = (
+				0D02163B27BC7B48009BD76F /* nullable_returns.gen.h */,
+				0D02163C27BC7B48009BD76F /* nullable_returns.gen.m */,
 				0DBD8C40279B741800E4FDBA /* non_null_fields.gen.h */,
 				0DBD8C3F279B741800E4FDBA /* non_null_fields.gen.m */,
 				0DA5DFD426CC39D600D2354B /* multiple_arity.gen.h */,
@@ -411,6 +416,7 @@
 				0DD2E6BE2684031300A7D764 /* message.gen.m in Sources */,
 				0DD2E6BA2684031300A7D764 /* void_arg_host.gen.m in Sources */,
 				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+				0D02163D27BC7B48009BD76F /* nullable_returns.gen.m in Sources */,
 				0DD2E6BF2684031300A7D764 /* enum.gen.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
index e3dba64..686205b 100644
--- a/packages/pigeon/pubspec.yaml
+++ b/packages/pigeon/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Code generator tool to make communication between Flutter and the host platform type-safe and easier.
 repository: https://github.com/flutter/packages/tree/main/packages/pigeon
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3Apigeon
-version: 1.0.18 # This must match the version in lib/generator_tools.dart
+version: 1.0.19 # This must match the version in lib/generator_tools.dart
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/pigeon/run_tests.sh b/packages/pigeon/run_tests.sh
index a44a34b..60f3971 100755
--- a/packages/pigeon/run_tests.sh
+++ b/packages/pigeon/run_tests.sh
@@ -241,6 +241,7 @@
   gen_ios_unittests_code ./pigeons/message.dart ""
   gen_ios_unittests_code ./pigeons/multiple_arity.dart ""
   gen_ios_unittests_code ./pigeons/non_null_fields.dart "NNF"
+  gen_ios_unittests_code ./pigeons/nullable_returns.dart "NR"
   gen_ios_unittests_code ./pigeons/primitive.dart ""
   gen_ios_unittests_code ./pigeons/void_arg_flutter.dart "VAF"
   gen_ios_unittests_code ./pigeons/void_arg_host.dart "VAH"
@@ -300,6 +301,7 @@
   gen_android_unittests_code ./pigeons/message.dart MessagePigeon
   gen_android_unittests_code ./pigeons/multiple_arity.dart MultipleArity
   gen_android_unittests_code ./pigeons/non_null_fields.dart NonNullFields
+  gen_android_unittests_code ./pigeons/nullable_returns.dart NullableReturns
   gen_android_unittests_code ./pigeons/primitive.dart Primitive
   gen_android_unittests_code ./pigeons/void_arg_flutter.dart VoidArgFlutter
   gen_android_unittests_code ./pigeons/void_arg_host.dart VoidArgHost
diff --git a/packages/pigeon/test/dart_generator_test.dart b/packages/pigeon/test/dart_generator_test.dart
index 8a9dc51..647376b 100644
--- a/packages/pigeon/test/dart_generator_test.dart
+++ b/packages/pigeon/test/dart_generator_test.dart
@@ -945,4 +945,123 @@
             'final List<int?>? arg_foo = (args[0] as List<Object?>?)?.cast<int?>()'));
     expect(code, contains('final List<int?> output = api.doit(arg_foo!)'));
   });
+
+  test('return nullable host', () {
+    final Root root = Root(
+      apis: <Api>[
+        Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+          Method(
+              name: 'doit',
+              returnType: const TypeDeclaration(
+                baseName: 'int',
+                isNullable: true,
+              ),
+              arguments: <NamedType>[])
+        ])
+      ],
+      classes: <Class>[],
+      enums: <Enum>[],
+    );
+    final StringBuffer sink = StringBuffer();
+    generateDart(const DartOptions(isNullSafe: true), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('Future<int?> doit()'));
+    expect(code, contains('return (replyMap[\'result\'] as int?);'));
+  });
+
+  test('return nullable async host', () {
+    final Root root = Root(
+      apis: <Api>[
+        Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+          Method(
+              name: 'doit',
+              returnType: const TypeDeclaration(
+                baseName: 'int',
+                isNullable: true,
+              ),
+              arguments: <NamedType>[],
+              isAsynchronous: true)
+        ])
+      ],
+      classes: <Class>[],
+      enums: <Enum>[],
+    );
+    final StringBuffer sink = StringBuffer();
+    generateDart(const DartOptions(isNullSafe: true), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('Future<int?> doit()'));
+    expect(code, contains('return (replyMap[\'result\'] as int?);'));
+  });
+
+  test('return nullable flutter', () {
+    final Root root = Root(
+      apis: <Api>[
+        Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+          Method(
+              name: 'doit',
+              returnType: const TypeDeclaration(
+                baseName: 'int',
+                isNullable: true,
+              ),
+              arguments: <NamedType>[])
+        ])
+      ],
+      classes: <Class>[],
+      enums: <Enum>[],
+    );
+    final StringBuffer sink = StringBuffer();
+    generateDart(const DartOptions(isNullSafe: true), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('int? doit();'));
+    expect(code, contains('final int? output = api.doit();'));
+  });
+
+  test('return nullable async flutter', () {
+    final Root root = Root(
+      apis: <Api>[
+        Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+          Method(
+              name: 'doit',
+              returnType: const TypeDeclaration(
+                baseName: 'int',
+                isNullable: true,
+              ),
+              arguments: <NamedType>[],
+              isAsynchronous: true)
+        ])
+      ],
+      classes: <Class>[],
+      enums: <Enum>[],
+    );
+    final StringBuffer sink = StringBuffer();
+    generateDart(const DartOptions(isNullSafe: true), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('Future<int?> doit();'));
+    expect(code, contains('final int? output = await api.doit();'));
+  });
+
+  test('platform error for return nil on nonnull', () {
+    final Root root = Root(
+      apis: <Api>[
+        Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+          Method(
+              name: 'doit',
+              returnType: const TypeDeclaration(
+                baseName: 'int',
+                isNullable: false,
+              ),
+              arguments: <NamedType>[])
+        ])
+      ],
+      classes: <Class>[],
+      enums: <Enum>[],
+    );
+    final StringBuffer sink = StringBuffer();
+    generateDart(const DartOptions(isNullSafe: true), root, sink);
+    final String code = sink.toString();
+    expect(
+        code,
+        contains(
+            'Host platform returned null value for non-null return value.'));
+  });
 }
diff --git a/packages/pigeon/test/java_generator_test.dart b/packages/pigeon/test/java_generator_test.dart
index f162d3c..95f4652 100644
--- a/packages/pigeon/test/java_generator_test.dart
+++ b/packages/pigeon/test/java_generator_test.dart
@@ -874,4 +874,52 @@
         contains(
             'channel.send(new ArrayList<Object>(Arrays.asList(xArg, yArg)), channelReply ->'));
   });
+
+  test('return nullable host', () {
+    final Root root = Root(
+      apis: <Api>[
+        Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+          Method(
+              name: 'doit',
+              returnType: const TypeDeclaration(
+                baseName: 'int',
+                isNullable: true,
+              ),
+              arguments: <NamedType>[])
+        ])
+      ],
+      classes: <Class>[],
+      enums: <Enum>[],
+    );
+    final StringBuffer sink = StringBuffer();
+    const JavaOptions javaOptions = JavaOptions(className: 'Messages');
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    expect(code, contains('@Nullable Long doit();'));
+  });
+
+  test('return nullable host async', () {
+    final Root root = Root(
+      apis: <Api>[
+        Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+          Method(
+              name: 'doit',
+              returnType: const TypeDeclaration(
+                baseName: 'int',
+                isNullable: true,
+              ),
+              isAsynchronous: true,
+              arguments: <NamedType>[])
+        ])
+      ],
+      classes: <Class>[],
+      enums: <Enum>[],
+    );
+    final StringBuffer sink = StringBuffer();
+    const JavaOptions javaOptions = JavaOptions(className: 'Messages');
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    // Java doesn't accept nullability annotations in type arguments.
+    expect(code, contains('Result<Long>'));
+  });
 }
diff --git a/packages/pigeon/test/objc_generator_test.dart b/packages/pigeon/test/objc_generator_test.dart
index f534441..c03738c 100644
--- a/packages/pigeon/test/objc_generator_test.dart
+++ b/packages/pigeon/test/objc_generator_test.dart
@@ -187,6 +187,7 @@
     expect(code, contains('@interface Input'));
     expect(code, contains('@interface Output'));
     expect(code, contains('@protocol Api'));
+    expect(code, contains('/// @return `nil` only when `error != nil`.'));
     expect(code, matches('nullable Output.*doSomething.*Input.*FlutterError'));
     expect(code, matches('ApiSetup.*<Api>.*_Nullable'));
   });
@@ -748,7 +749,7 @@
     expect(
         code,
         contains(
-            '(void)doSomethingWithCompletion:(void(^)(ABCOutput *, NSError *_Nullable))completion'));
+            '(void)doSomethingWithCompletion:(void(^)(ABCOutput *_Nullable, NSError *_Nullable))completion'));
   });
 
   test('gen flutter void arg source', () {
@@ -775,7 +776,7 @@
     expect(
         code,
         contains(
-            '(void)doSomethingWithCompletion:(void(^)(ABCOutput *, NSError *_Nullable))completion'));
+            '(void)doSomethingWithCompletion:(void(^)(ABCOutput *_Nullable, NSError *_Nullable))completion'));
     expect(code, contains('channel sendMessage:nil'));
   });
 
@@ -1484,7 +1485,7 @@
       expect(
           code,
           contains(
-              '- (void)addX:(NSNumber *)x y:(NSNumber *)y completion:(void(^)(NSNumber *, NSError *_Nullable))completion;'));
+              '- (void)addX:(NSNumber *)x y:(NSNumber *)y completion:(void(^)(NSNumber *_Nullable, NSError *_Nullable))completion;'));
     }
     {
       final StringBuffer sink = StringBuffer();
@@ -1494,7 +1495,7 @@
       expect(
           code,
           contains(
-              '- (void)addX:(NSNumber *)arg_x y:(NSNumber *)arg_y completion:(void(^)(NSNumber *, NSError *_Nullable))completion {'));
+              '- (void)addX:(NSNumber *)arg_x y:(NSNumber *)arg_y completion:(void(^)(NSNumber *_Nullable, NSError *_Nullable))completion {'));
       expect(code, contains('[channel sendMessage:@[arg_x, arg_y] reply:'));
     }
   });
@@ -1582,4 +1583,73 @@
     expect(code, contains('@interface Foobar'));
     expect(code, contains('@property(nonatomic, copy) NSString * field1'));
   });
+
+  test('return nullable flutter header', () {
+    final Root root = Root(
+      apis: <Api>[
+        Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+          Method(
+              name: 'doit',
+              returnType: const TypeDeclaration(
+                baseName: 'int',
+                isNullable: true,
+              ),
+              arguments: <NamedType>[])
+        ])
+      ],
+      classes: <Class>[],
+      enums: <Enum>[],
+    );
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(const ObjcOptions(), root, sink);
+    final String code = sink.toString();
+    expect(
+        code,
+        matches(
+            r'doitWithCompletion.*void.*NSNumber \*_Nullable.*NSError.*completion;'));
+  });
+
+  test('return nullable flutter source', () {
+    final Root root = Root(
+      apis: <Api>[
+        Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+          Method(
+              name: 'doit',
+              returnType: const TypeDeclaration(
+                baseName: 'int',
+                isNullable: true,
+              ),
+              arguments: <NamedType>[])
+        ])
+      ],
+      classes: <Class>[],
+      enums: <Enum>[],
+    );
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(const ObjcOptions(), root, sink);
+    final String code = sink.toString();
+    expect(code, matches(r'doitWithCompletion.*NSNumber \*_Nullable'));
+  });
+
+  test('return nullable host header', () {
+    final Root root = Root(
+      apis: <Api>[
+        Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+          Method(
+              name: 'doit',
+              returnType: const TypeDeclaration(
+                baseName: 'int',
+                isNullable: true,
+              ),
+              arguments: <NamedType>[])
+        ])
+      ],
+      classes: <Class>[],
+      enums: <Enum>[],
+    );
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(const ObjcOptions(), root, sink);
+    final String code = sink.toString();
+    expect(code, matches(r'nullable NSNumber.*doitWithError'));
+  });
 }
diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart
index 512b165..1d21f46 100644
--- a/packages/pigeon/test/pigeon_lib_test.dart
+++ b/packages/pigeon/test/pigeon_lib_test.dart
@@ -581,23 +581,6 @@
     expect(results.errors[0].message, contains('Nullable'));
   });
 
-  test('nullable api return', () {
-    const String code = '''
-class Foo {
-  int? x;
-}
-
-@HostApi()
-abstract class Api {
-  Foo? doit(Foo foo);
-}
-''';
-    final ParseResults results = _parseSource(code);
-    expect(results.errors.length, 1);
-    expect(results.errors[0].lineNumber, 7);
-    expect(results.errors[0].message, contains('Nullable'));
-  });
-
   test('test invalid import', () {
     const String code = 'import \'foo.dart\';\n';
     final ParseResults results = _parseSource(code);
@@ -1049,4 +1032,17 @@
     final PigeonOptions options = PigeonOptions.fromMap(results.pigeonOptions!);
     expect(options.objcOptions!.copyrightHeader, <String>['A', 'Header']);
   });
+
+  test('return nullable', () {
+    const String code = '''
+@HostApi()
+abstract class Api {
+  int? calc();
+}
+''';
+
+    final ParseResults results = _parseSource(code);
+    expect(results.errors.length, 0);
+    expect(results.root.apis[0].methods[0].returnType.isNullable, isTrue);
+  });
 }