diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index 9c13251231..0318441b7e 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -32,6 +32,7 @@
 #include "common/keywords.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "nodes/miscnodes.h"
 #include "parser/scansup.h"
 #include "pgstat.h"
 #include "postmaster/syslogger.h"
@@ -45,6 +46,22 @@
 #include "utils/ruleutils.h"
 #include "utils/timestamp.h"
 
+/*
+ * structure to cache metadata needed in pg_input_is_valid_common
+ */
+typedef struct BasicIOData
+{
+	Oid			typoid;
+	Oid			typiofunc;
+	Oid			typioparam;
+	FmgrInfo	proc;
+} BasicIOData;
+
+static bool pg_input_is_valid_common(FunctionCallInfo fcinfo,
+									 text *txt, Oid typoid, int32 typmod,
+									 ErrorSaveContext *escontext);
+
+
 /*
  * Common subroutine for num_nulls() and num_nonnulls().
  * Returns true if successful, false if function should return NULL.
@@ -640,6 +657,146 @@ pg_column_is_updatable(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * pg_input_is_valid - test whether string is valid input for datatype.
+ *
+ * Returns true if OK, false if not.
+ *
+ * This will only work usefully if the datatype's input function has been
+ * updated to return "safe" errors via errsave/ereturn.
+ */
+Datum
+pg_input_is_valid(PG_FUNCTION_ARGS)
+{
+	text	   *txt = PG_GETARG_TEXT_PP(0);
+	Oid			typoid = PG_GETARG_OID(1);
+	ErrorSaveContext escontext;
+
+	/* Set up empty ErrorSaveContext */
+	memset(&escontext, 0, sizeof(escontext));
+	escontext.type = T_ErrorSaveContext;
+
+	PG_RETURN_BOOL(pg_input_is_valid_common(fcinfo, txt, typoid, -1,
+											&escontext));
+}
+
+/* Same, with non-default typmod */
+Datum
+pg_input_is_valid_mod(PG_FUNCTION_ARGS)
+{
+	text	   *txt = PG_GETARG_TEXT_PP(0);
+	Oid			typoid = PG_GETARG_OID(1);
+	int32		typmod = PG_GETARG_INT32(2);
+	ErrorSaveContext escontext;
+
+	/* Set up empty ErrorSaveContext */
+	memset(&escontext, 0, sizeof(escontext));
+	escontext.type = T_ErrorSaveContext;
+
+	PG_RETURN_BOOL(pg_input_is_valid_common(fcinfo, txt, typoid, typmod,
+											&escontext));
+}
+
+/*
+ * pg_input_invalid_message - test whether string is valid input for datatype.
+ *
+ * Returns NULL if OK, else the primary message string from the error.
+ *
+ * This will only work usefully if the datatype's input function has been
+ * updated to return "safe" errors via errsave/ereturn.
+ */
+Datum
+pg_input_invalid_message(PG_FUNCTION_ARGS)
+{
+	text	   *txt = PG_GETARG_TEXT_PP(0);
+	Oid			typoid = PG_GETARG_OID(1);
+	ErrorSaveContext escontext;
+
+	/* Set up empty ErrorSaveContext, but enable details_wanted */
+	memset(&escontext, 0, sizeof(escontext));
+	escontext.type = T_ErrorSaveContext;
+	escontext.details_wanted = true;
+
+	if (pg_input_is_valid_common(fcinfo, txt, typoid, -1,
+								 &escontext))
+		PG_RETURN_NULL();
+
+	Assert(escontext.error_occurred);
+	Assert(escontext.error_data != NULL);
+	Assert(escontext.error_data->message != NULL);
+
+	PG_RETURN_TEXT_P(cstring_to_text(escontext.error_data->message));
+}
+
+/* Same, with non-default typmod */
+Datum
+pg_input_invalid_message_mod(PG_FUNCTION_ARGS)
+{
+	text	   *txt = PG_GETARG_TEXT_PP(0);
+	Oid			typoid = PG_GETARG_OID(1);
+	int32		typmod = PG_GETARG_INT32(2);
+	ErrorSaveContext escontext;
+
+	/* Set up empty ErrorSaveContext, but enable details_wanted */
+	memset(&escontext, 0, sizeof(escontext));
+	escontext.type = T_ErrorSaveContext;
+	escontext.details_wanted = true;
+
+	if (pg_input_is_valid_common(fcinfo, txt, typoid, typmod,
+								 &escontext))
+		PG_RETURN_NULL();
+
+	Assert(escontext.error_occurred);
+	Assert(escontext.error_data != NULL);
+	Assert(escontext.error_data->message != NULL);
+
+	PG_RETURN_TEXT_P(cstring_to_text(escontext.error_data->message));
+}
+
+/* Common subroutine for the above */
+static bool
+pg_input_is_valid_common(FunctionCallInfo fcinfo,
+						 text *txt, Oid typoid, int32 typmod,
+						 ErrorSaveContext *escontext)
+{
+	char	   *str = text_to_cstring(txt);
+	BasicIOData *my_extra;
+	Datum		converted;
+
+	/*
+	 * We arrange to look up the needed I/O info just once per series of
+	 * calls, assuming the data type doesn't change underneath us.
+	 */
+	my_extra = (BasicIOData *) fcinfo->flinfo->fn_extra;
+	if (my_extra == NULL)
+	{
+		fcinfo->flinfo->fn_extra =
+			MemoryContextAlloc(fcinfo->flinfo->fn_mcxt,
+							   sizeof(BasicIOData));
+		my_extra = (BasicIOData *) fcinfo->flinfo->fn_extra;
+		my_extra->typoid = InvalidOid;
+	}
+
+	if (my_extra->typoid != typoid)
+	{
+		getTypeInputInfo(typoid,
+						 &my_extra->typiofunc,
+						 &my_extra->typioparam);
+		fmgr_info_cxt(my_extra->typiofunc, &my_extra->proc,
+					  fcinfo->flinfo->fn_mcxt);
+		my_extra->typoid = typoid;
+	}
+
+	/* Now we can try to perform the conversion */
+	return InputFunctionCallSafe(&my_extra->proc,
+								 str,
+								 my_extra->typioparam,
+								 typmod,
+								 (Node *) escontext,
+								 &converted);
+}
+
+
 /*
  * Is character a valid identifier start?
  * Must match scan.l's {ident_start} character class.
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f9301b2627..d178a5a8ec 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7060,6 +7060,23 @@
   prorettype => 'regnamespace', proargtypes => 'text',
   prosrc => 'to_regnamespace' },
 
+{ oid => '8050', descr => 'test whether string is valid input for data type',
+  proname => 'pg_input_is_valid', provolatile => 's', prorettype => 'bool',
+  proargtypes => 'text regtype', prosrc => 'pg_input_is_valid' },
+{ oid => '8051', descr => 'test whether string is valid input for data type',
+  proname => 'pg_input_is_valid', provolatile => 's', prorettype => 'bool',
+  proargtypes => 'text regtype int4', prosrc => 'pg_input_is_valid_mod' },
+{ oid => '8052',
+  descr => 'get error message if string is not valid input for data type',
+  proname => 'pg_input_invalid_message', provolatile => 's',
+  prorettype => 'text', proargtypes => 'text regtype',
+  prosrc => 'pg_input_invalid_message' },
+{ oid => '8053',
+  descr => 'get error message if string is not valid input for data type',
+  proname => 'pg_input_invalid_message', provolatile => 's',
+  prorettype => 'text', proargtypes => 'text regtype int4',
+  prosrc => 'pg_input_invalid_message_mod' },
+
 { oid => '1268',
   descr => 'parse qualified identifier to array of identifiers',
   proname => 'parse_ident', prorettype => '_text', proargtypes => 'text bool',
diff --git a/src/test/regress/expected/arrays.out b/src/test/regress/expected/arrays.out
index 97920f38c2..5253541470 100644
--- a/src/test/regress/expected/arrays.out
+++ b/src/test/regress/expected/arrays.out
@@ -182,6 +182,31 @@ SELECT a,b,c FROM arrtest;
  [4:4]={NULL}  | {3,4}                 | {foo,new_word}
 (3 rows)
 
+-- test non-error-throwing API
+SELECT pg_input_is_valid('{1,2,3}', 'integer[]');
+ pg_input_is_valid 
+-------------------
+ t
+(1 row)
+
+SELECT pg_input_is_valid('{1,2', 'integer[]');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT pg_input_is_valid('{1,zed}', 'integer[]');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT pg_input_invalid_message('{1,zed}', 'integer[]');
+           pg_input_invalid_message           
+----------------------------------------------
+ invalid input syntax for type integer: "zed"
+(1 row)
+
 -- test mixed slice/scalar subscripting
 select '{{1,2,3},{4,5,6},{7,8,9}}'::int[];
            int4            
diff --git a/src/test/regress/expected/boolean.out b/src/test/regress/expected/boolean.out
index 4728fe2dfd..08c7e45803 100644
--- a/src/test/regress/expected/boolean.out
+++ b/src/test/regress/expected/boolean.out
@@ -142,6 +142,25 @@ SELECT bool '' AS error;
 ERROR:  invalid input syntax for type boolean: ""
 LINE 1: SELECT bool '' AS error;
                     ^
+-- Also try it with non-error-throwing API
+SELECT pg_input_is_valid('true', 'bool');
+ pg_input_is_valid 
+-------------------
+ t
+(1 row)
+
+SELECT pg_input_is_valid('asdf', 'bool');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT pg_input_invalid_message('junk', 'bool');
+           pg_input_invalid_message            
+-----------------------------------------------
+ invalid input syntax for type boolean: "junk"
+(1 row)
+
 -- and, or, not in qualifications
 SELECT bool 't' or bool 'f' AS true;
  true 
diff --git a/src/test/regress/expected/create_type.out b/src/test/regress/expected/create_type.out
index 0dfc88c1c8..7383fcdbb1 100644
--- a/src/test/regress/expected/create_type.out
+++ b/src/test/regress/expected/create_type.out
@@ -249,6 +249,31 @@ select format_type('bpchar'::regtype, -1);
  bpchar
 (1 row)
 
+-- Test non-error-throwing APIs using widget, which still throws errors
+SELECT pg_input_is_valid('(1,2,3)', 'widget');
+ pg_input_is_valid 
+-------------------
+ t
+(1 row)
+
+SELECT pg_input_is_valid('(1,2)', 'widget');  -- hard error expected
+ERROR:  invalid input syntax for type widget: "(1,2)"
+SELECT pg_input_is_valid('{"(1,2,3)"}', 'widget[]');
+ pg_input_is_valid 
+-------------------
+ t
+(1 row)
+
+SELECT pg_input_is_valid('{"(1,2)"}', 'widget[]');  -- hard error expected
+ERROR:  invalid input syntax for type widget: "(1,2)"
+SELECT pg_input_is_valid('("(1,2,3)")', 'mytab');
+ pg_input_is_valid 
+-------------------
+ t
+(1 row)
+
+SELECT pg_input_is_valid('("(1,2)")', 'mytab');  -- hard error expected
+ERROR:  invalid input syntax for type widget: "(1,2)"
 -- Test creation of an operator over a user-defined type
 CREATE FUNCTION pt_in_widget(point, widget)
    RETURNS bool
diff --git a/src/test/regress/expected/int4.out b/src/test/regress/expected/int4.out
index fbcc0e8d9e..cc5dfc092c 100644
--- a/src/test/regress/expected/int4.out
+++ b/src/test/regress/expected/int4.out
@@ -45,6 +45,31 @@ SELECT * FROM INT4_TBL;
  -2147483647
 (5 rows)
 
+-- Also try it with non-error-throwing API
+SELECT pg_input_is_valid('34', 'int4');
+ pg_input_is_valid 
+-------------------
+ t
+(1 row)
+
+SELECT pg_input_is_valid('asdf', 'int4');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT pg_input_is_valid('1000000000000', 'int4');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT pg_input_invalid_message('1000000000000', 'int4');
+                pg_input_invalid_message                
+--------------------------------------------------------
+ value "1000000000000" is out of range for type integer
+(1 row)
+
 SELECT i.* FROM INT4_TBL i WHERE i.f1 <> int2 '0';
      f1      
 -------------
diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index a4cc2d8c12..47d4fe7518 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -69,6 +69,32 @@ ERROR:  malformed record literal: "(Joe,Blow) /"
 LINE 1: select '(Joe,Blow) /'::fullname;
                ^
 DETAIL:  Junk after right parenthesis.
+-- test non-error-throwing API
+create type twoints as (r integer, i integer);
+SELECT pg_input_is_valid('(1,2)', 'twoints');
+ pg_input_is_valid 
+-------------------
+ t
+(1 row)
+
+SELECT pg_input_is_valid('(1,2', 'twoints');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT pg_input_is_valid('(1,zed)', 'twoints');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT pg_input_invalid_message('(1,zed)', 'twoints');
+           pg_input_invalid_message           
+----------------------------------------------
+ invalid input syntax for type integer: "zed"
+(1 row)
+
 create temp table quadtable(f1 int, q quad);
 insert into quadtable values (1, ((3.3,4.4),(5.5,6.6)));
 insert into quadtable values (2, ((null,4.4),(5.5,6.6)));
diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c
index 548afb4438..d6e6733670 100644
--- a/src/test/regress/regress.c
+++ b/src/test/regress/regress.c
@@ -183,6 +183,11 @@ widget_in(PG_FUNCTION_ARGS)
 			coord[i++] = p + 1;
 	}
 
+	/*
+	 * Note: DON'T convert this error to "safe" style (errsave/ereturn).  We
+	 * want this data type to stay permanently in the hard-error world so that
+	 * it can be used for testing that such cases still work reasonably.
+	 */
 	if (i < NARGS)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
diff --git a/src/test/regress/sql/arrays.sql b/src/test/regress/sql/arrays.sql
index 791af5c0ce..cb91458b74 100644
--- a/src/test/regress/sql/arrays.sql
+++ b/src/test/regress/sql/arrays.sql
@@ -113,6 +113,12 @@ SELECT a FROM arrtest WHERE a[2] IS NULL;
 DELETE FROM arrtest WHERE a[2] IS NULL AND b IS NULL;
 SELECT a,b,c FROM arrtest;
 
+-- test non-error-throwing API
+SELECT pg_input_is_valid('{1,2,3}', 'integer[]');
+SELECT pg_input_is_valid('{1,2', 'integer[]');
+SELECT pg_input_is_valid('{1,zed}', 'integer[]');
+SELECT pg_input_invalid_message('{1,zed}', 'integer[]');
+
 -- test mixed slice/scalar subscripting
 select '{{1,2,3},{4,5,6},{7,8,9}}'::int[];
 select ('{{1,2,3},{4,5,6},{7,8,9}}'::int[])[1:2][2];
diff --git a/src/test/regress/sql/boolean.sql b/src/test/regress/sql/boolean.sql
index 4dd47aaf9d..fc463d705c 100644
--- a/src/test/regress/sql/boolean.sql
+++ b/src/test/regress/sql/boolean.sql
@@ -62,6 +62,11 @@ SELECT bool '000' AS error;
 
 SELECT bool '' AS error;
 
+-- Also try it with non-error-throwing API
+SELECT pg_input_is_valid('true', 'bool');
+SELECT pg_input_is_valid('asdf', 'bool');
+SELECT pg_input_invalid_message('junk', 'bool');
+
 -- and, or, not in qualifications
 
 SELECT bool 't' or bool 'f' AS true;
diff --git a/src/test/regress/sql/create_type.sql b/src/test/regress/sql/create_type.sql
index c6fc4f9029..c25018029c 100644
--- a/src/test/regress/sql/create_type.sql
+++ b/src/test/regress/sql/create_type.sql
@@ -192,6 +192,14 @@ select format_type('bpchar'::regtype, null);
 -- this behavior difference is intentional
 select format_type('bpchar'::regtype, -1);
 
+-- Test non-error-throwing APIs using widget, which still throws errors
+SELECT pg_input_is_valid('(1,2,3)', 'widget');
+SELECT pg_input_is_valid('(1,2)', 'widget');  -- hard error expected
+SELECT pg_input_is_valid('{"(1,2,3)"}', 'widget[]');
+SELECT pg_input_is_valid('{"(1,2)"}', 'widget[]');  -- hard error expected
+SELECT pg_input_is_valid('("(1,2,3)")', 'mytab');
+SELECT pg_input_is_valid('("(1,2)")', 'mytab');  -- hard error expected
+
 -- Test creation of an operator over a user-defined type
 
 CREATE FUNCTION pt_in_widget(point, widget)
diff --git a/src/test/regress/sql/int4.sql b/src/test/regress/sql/int4.sql
index f19077f3da..a188731178 100644
--- a/src/test/regress/sql/int4.sql
+++ b/src/test/regress/sql/int4.sql
@@ -17,6 +17,12 @@ INSERT INTO INT4_TBL(f1) VALUES ('');
 
 SELECT * FROM INT4_TBL;
 
+-- Also try it with non-error-throwing API
+SELECT pg_input_is_valid('34', 'int4');
+SELECT pg_input_is_valid('asdf', 'int4');
+SELECT pg_input_is_valid('1000000000000', 'int4');
+SELECT pg_input_invalid_message('1000000000000', 'int4');
+
 SELECT i.* FROM INT4_TBL i WHERE i.f1 <> int2 '0';
 
 SELECT i.* FROM INT4_TBL i WHERE i.f1 <> int4 '0';
diff --git a/src/test/regress/sql/rowtypes.sql b/src/test/regress/sql/rowtypes.sql
index ad5b7e128f..d558d66eb6 100644
--- a/src/test/regress/sql/rowtypes.sql
+++ b/src/test/regress/sql/rowtypes.sql
@@ -31,6 +31,13 @@ select '[]'::fullname;          -- bad
 select ' (Joe,Blow)  '::fullname;  -- ok, extra whitespace
 select '(Joe,Blow) /'::fullname;  -- bad
 
+-- test non-error-throwing API
+create type twoints as (r integer, i integer);
+SELECT pg_input_is_valid('(1,2)', 'twoints');
+SELECT pg_input_is_valid('(1,2', 'twoints');
+SELECT pg_input_is_valid('(1,zed)', 'twoints');
+SELECT pg_input_invalid_message('(1,zed)', 'twoints');
+
 create temp table quadtable(f1 int, q quad);
 
 insert into quadtable values (1, ((3.3,4.4),(5.5,6.6)));
